diff --git a/CLAUDE.md b/CLAUDE.md index 6bddf3d..850b193 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,10 @@ Default canonical labels: `needs-triage`, `needs-info`, `ready-for-agent`, `read Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. +### Writing style + +Standards for help text, the README, and SKILL.md; inline code in help prose is always backticked (test-enforced). See `docs/agents/writing-style.md`. + ## Development Setup, from-source install, the design-doc map, and the release process live in `CONTRIBUTING.md`. The README is user-facing only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b725b15..e9c8289 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,10 @@ no Python required. ``` - `docs/telemetry.md`, `docs/user-agent.md` — wire-visible contracts. +- `docs/agents/writing-style.md` — standards for help text, the README, + and SKILL.md. Backticked inline code in help prose is enforced by + `tests/test_help_style.py`; the hosted docs generate their CLI + reference from `help --json`, so these strings are customer-facing. ## Releasing diff --git a/docs/agents/writing-style.md b/docs/agents/writing-style.md new file mode 100644 index 0000000..82a066e --- /dev/null +++ b/docs/agents/writing-style.md @@ -0,0 +1,36 @@ +# Writing style + +Standards for every string a user or an agent reads: help text and +docstrings (which become `ade help` and the hosted CLI reference), the +README, and SKILL.md. + +## Prose standards + +- Second person ("you"), active voice, direct language. +- No promotional language. Describe what a thing does. +- No idioms. Plain wording wins. +- Test every code example before it lands. Fence README and SKILL.md + code blocks with a language tag. +- Error and status messages state what happened and what to do next. + +## Inline code is always backticked (enforced) + +Wrap flags (`--api-key`), commands (`ade update`), paths +(`~/.ade/credentials.json`), environment variables (`ADE_ENV`), and +literal values (`priority`) in backticks wherever they appear in prose. + +Why: the hosted docs generate the CLI reference from `help --json` and +render a bare `--flag` as an em dash plus the flag name; agents need +unambiguous literal boundaries; and terminal output prints backticks +literally, which is already this CLI's voice. + +`tests/test_help_style.py` enforces the flag rule across the help +surface's prose fields. Pre-formatted blocks are exempt: topic bodies +and usage lines are terminal layouts and copy-pasteable commands, so +leave them bare. + +## Relationship to the docs repo + +The docs repo (landing-ai/docs) has stricter rules for hosted pages. +This file governs text that lives in this repo; the docs repo's +generator is the compatibility layer between the two. diff --git a/docs/reference/help.json b/docs/reference/help.json index e7904b5..f68a2b1 100644 --- a/docs/reference/help.json +++ b/docs/reference/help.json @@ -4,11 +4,11 @@ "conventions": [ { "name": "--json", - "rule": "Every command supports --json: one stable JSON object/array on stdout (errors and pending payloads follow the same rule). Agents should always pass it. Each command's published shape is its 'result' block below \u2014 the full result is always on stdout, never only in a file." + "rule": "Every command supports `--json`: one stable JSON object/array on stdout (errors and pending payloads follow the same rule). Agents should always pass it. Each command's published shape is its 'result' block below \u2014 the full result is always on stdout, never only in a file." }, { "name": "--id-only", - "rule": "parse, extract, and find also take --id-only: just the id(s), one per line, for piping (JOB=$(ade parse -d f.pdf --id-only)). Errors and hints go to stderr so a captured id is never a sentence." + "rule": "parse, extract, and find also take `--id-only`: just the id(s), one per line, for piping (`JOB=$(ade parse -d f.pdf --id-only)`). Errors and hints go to stderr so a captured id is never a sentence." }, { "name": "job item ids", @@ -16,7 +16,7 @@ }, { "name": "guarantees", - "rule": "parse and extract ensure a run exists rather than fire a request: an already-done run is served from disk free with an explicit notice (--force consents to a re-bill); a pending run is resumed, never resubmitted; Ctrl-C stops the waiting, not the work." + "rule": "parse and extract ensure a run exists rather than fire a request: an already-done run is served from disk free with an explicit notice (`--force` consents to a re-bill); a pending run is resumed, never resubmitted; Ctrl-C stops the waiting, not the work." }, { "name": "env overrides", @@ -27,7 +27,7 @@ { "name": "auth login", "usage": "ade auth login [options] [--json]", - "summary": "Ensure the target environment is logged in; --api-key\nauthenticates with a key directly ('-' prompts with hidden input).\nTargets --env, else $ADE_ENV, else production \u2014 nothing is stored\nabout the choice.", + "summary": "Ensure the target environment is logged in; `--api-key`\nauthenticates with a key directly ('-' prompts with hidden input).\nTargets `--env`, else $ADE_ENV, else production \u2014 nothing is stored\nabout the choice.", "arguments": [], "flags": [ { @@ -138,7 +138,7 @@ { "name": "auth logout", "usage": "ade auth logout [options] [--json]", - "summary": "Log out of one environment (the resolved target by default); --all\nclears every environment. Idempotent; OAuth refresh tokens are revoked\nbest-effort first.", + "summary": "Log out of one environment (the resolved target by default); `--all`\nclears every environment. Idempotent; OAuth refresh tokens are revoked\nbest-effort first.", "arguments": [], "flags": [ { @@ -178,7 +178,7 @@ }, { "key": "environment", - "what": "the environment cleared (null with --all)" + "what": "the environment cleared (null with `--all`)" } ] }, @@ -187,7 +187,7 @@ { "name": "login", "usage": "ade login [options] [--json]", - "summary": "Alias of `ade auth login`: ensure the target environment is logged in; --api-key authenticates with a key directly ('-' prompts with hidden input).", + "summary": "Alias of `ade auth login`: ensure the target environment is logged in; `--api-key` authenticates with a key directly ('-' prompts with hidden input).", "arguments": [], "flags": [ { @@ -248,7 +248,7 @@ { "name": "logout", "usage": "ade logout [options] [--json]", - "summary": "Alias of `ade auth logout`: log out of one environment (the resolved target by default); --all clears every environment.", + "summary": "Alias of `ade auth logout`: log out of one environment (the resolved target by default); `--all` clears every environment.", "arguments": [], "flags": [ { @@ -288,7 +288,7 @@ }, { "key": "environment", - "what": "the environment cleared (null with --all)" + "what": "the environment cleared (null with `--all`)" } ] }, @@ -357,7 +357,7 @@ { "name": "help", "usage": "ade help [COMMAND|TOPIC]... [--json]", - "summary": "Print the whole-surface command reference in one call: every\ncommand and flag, the output convention, each verb's result shape,\nexit states, and the store layout. The agent bootstrap \u2014 run this\n(with --json) before anything else. `help TOPIC` prints one\nconceptual page instead (workflow, output, credentials, errors).", + "summary": "Print the whole-surface command reference in one call: every\ncommand and flag, the output convention, each verb's result shape,\nexit states, and the store layout. The agent bootstrap \u2014 run this\n(with `--json`) before anything else. `help TOPIC` prints one\nconceptual page instead (workflow, output, credentials, errors).", "arguments": [ { "name": "[COMMAND|TOPIC]...", @@ -409,14 +409,14 @@ "metavar": "FILE", "required": false, "default": null, - "help": "Local document file to parse; pass exactly one of -d/--document or --document-url." + "help": "Local document file to parse; pass exactly one of -d/`--document` or `--document-url`." }, { "flags": "--document-url", "metavar": "TEXT", "required": false, "default": null, - "help": "Document URL the server fetches (identity is the URL x params; a re-run dedups even if the remote content changed \u2014 --force refreshes)." + "help": "Document URL the server fetches (identity is the URL x params; a re-run dedups even if the remote content changed \u2014 `--force` refreshes)." }, { "flags": "--model", @@ -444,7 +444,7 @@ "metavar": "TEXT", "required": false, "default": null, - "help": "Full ParseOptions pass-through as a JSON object, sent verbatim (the server rejects unknown keys with a 422). Keys as of this release \u2014 pages: 1-indexed integer array (default: all pages); atomic_grounding: bool (default true; false omits the per-line atomic_grounding field from every node); inline_markdown: bool (default false; true adds each node's own markdown slice inline); blocks..markdown: bool (default true; false suppresses that type's markdown; types: text, table, figure, marginalia, attestation, logo, scan_code, card); blocks.table.format: 'html' (default) or 'markdown'; password: always rejected (422) \u2014 decrypt PDFs before upload. Example: '{\"inline_markdown\": true, \"blocks\": {\"table\": {\"format\": \"markdown\"}}}'. Merges with --pages; giving pages in both is an error." + "help": "Full ParseOptions pass-through as a JSON object, sent verbatim (the server rejects unknown keys with a 422). Keys as of this release \u2014 pages: 1-indexed integer array (default: all pages); atomic_grounding: bool (default true; false omits the per-line atomic_grounding field from every node); inline_markdown: bool (default false; true adds each node's own markdown slice inline); blocks..markdown: bool (default true; false suppresses that type's markdown; types: text, table, figure, marginalia, attestation, logo, scan_code, card); blocks.table.format: 'html' (default) or 'markdown'; password: always rejected (422) \u2014 decrypt PDFs before upload. Example: '{\"inline_markdown\": true, \"blocks\": {\"table\": {\"format\": \"markdown\"}}}'. Merges with `--pages`; giving pages in both is an error." }, { "flags": "--env", @@ -479,7 +479,7 @@ "metavar": null, "required": false, "default": null, - "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf --id-only)). Takes precedence over --json; errors and hints go to stderr." + "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf `--id-only`)). Takes precedence over `--json`; errors and hints go to stderr." } ], "supports_json": true, @@ -540,11 +540,11 @@ }, { "key": "markdown", - "what": "the parse markdown \u2014 only with --include markdown" + "what": "the parse markdown \u2014 only with `--include markdown`" }, { "key": "elements", - "what": "the flat projection \u2014 only with --include elements" + "what": "the flat projection \u2014 only with `--include elements`" } ] }, @@ -616,7 +616,7 @@ "metavar": "TEXT", "required": false, "default": null, - "help": "Environment to run against: dev, staging, production, eu (default: $ADE_ENV, then production). The JOB_ITEM_ID form inherits the parse item's environment instead \u2014 its server-side parse job only exists there \u2014 and a conflicting --env is refused." + "help": "Environment to run against: dev, staging, production, eu (default: $ADE_ENV, then production). The JOB_ITEM_ID form inherits the parse item's environment instead \u2014 its server-side parse job only exists there \u2014 and a conflicting `--env` is refused." }, { "flags": "--wait", @@ -637,7 +637,7 @@ "metavar": null, "required": false, "default": null, - "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf --id-only)). Takes precedence over --json; errors and hints go to stderr." + "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf `--id-only`)). Takes precedence over `--json`; errors and hints go to stderr." } ], "supports_json": true, @@ -772,7 +772,7 @@ }, { "key": "stale", - "what": "extract items: true when the referenced parse was --force re-run after this extraction" + "what": "extract items: true when the referenced parse was `--force` re-run after this extraction" }, { "key": "created_at / completed_at", @@ -821,12 +821,12 @@ { "name": "find", "usage": "ade find [JOB_ITEM_ID] [QUERY] [options] [--json]", - "summary": "Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or --job\n(repeatable) for several items; no query lists every element.\n\nIds discovered here are what `view --element-id` deep-links and\n`crop --element-id` renders \u2014 though `crop` takes these same filters\ndirectly (`crop JOB_ITEM_ID --type figure`) when you want the images\nrather than the records.", + "summary": "Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or `--job`\n(repeatable) for several items; no query lists every element.\n\nIds discovered here are what `view --element-id` deep-links and\n`crop --element-id` renders \u2014 though `crop` takes these same filters\ndirectly (`crop JOB_ITEM_ID --type figure`) when you want the images\nrather than the records.", "arguments": [ { "name": "[JOB_ITEM_ID] [QUERY]", "required": false, - "help": "Job item id (or unambiguous prefix) to search, then an optional case-insensitive substring QUERY. With --job, the one allowed positional is the QUERY." + "help": "Job item id (or unambiguous prefix) to search, then an optional case-insensitive substring QUERY. With `--job`, the one allowed positional is the QUERY." } ], "flags": [ @@ -877,7 +877,7 @@ "metavar": null, "required": false, "default": null, - "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf --id-only)). Takes precedence over --json; errors and hints go to stderr." + "help": "Print only the id(s) this run produced, one per line \u2014 the piping mode (JOB=$(ade parse -d f.pdf `--id-only`)). Takes precedence over `--json`; errors and hints go to stderr." } ], "supports_json": true, @@ -909,7 +909,7 @@ "what": "the element's markdown slice" } ], - "note": "One record per match, in document order; [] when nothing matched. --id-only prints the element ids alone." + "note": "One record per match, in document order; [] when nothing matched. `--id-only` prints the element ids alone." }, "band": "local read models" }, @@ -937,21 +937,21 @@ "metavar": null, "required": false, "default": null, - "help": "With --element-id: render that element's PNG crop instead of the HTML artifact." + "help": "With `--element-id`: render that element's PNG crop instead of the HTML artifact." }, { "flags": "--open, --no-open", "metavar": null, "required": false, "default": null, - "help": "Open the result in the browser. Default: open when stdout is a terminal; --json runs and piped output never auto-open." + "help": "Open the result in the browser. Default: open when stdout is a terminal; `--json` runs and piped output never auto-open." }, { "flags": "--dpi", "metavar": "INTEGER", "required": false, "default": null, - "help": "Page render dpi (default 120); with --crop, the crop dpi (default 300)." + "help": "Page render dpi (default 120); with `--crop`, the crop dpi (default 300)." }, { "flags": "--pages", @@ -972,14 +972,14 @@ "metavar": null, "required": false, "default": null, - "help": "Open via a local server (http://127.0.0.1) instead of file:// \u2014 browser zoom then covers every viewer natively. Starts the server if needed; reuses a running one. It retires itself after 30 idle minutes, or immediately with --stop-server." + "help": "Open via a local server (http://127.0.0.1) instead of file:// \u2014 browser zoom then covers every viewer natively. Starts the server if needed; reuses a running one. It retires itself after 30 idle minutes, or immediately with `--stop-server`." }, { "flags": "--stop-server", "metavar": null, "required": false, "default": null, - "help": "Stop the local viewer server (started by --serve) and exit." + "help": "Stop the local viewer server (started by `--serve`) and exit." } ], "supports_json": true, @@ -988,7 +988,7 @@ "keys": [ { "key": "status", - "what": "'viewed' ('cropped' with --crop, 'synced' with --sync-viewers)" + "what": "'viewed' ('cropped' with `--crop`, 'synced' with `--sync-viewers`)" }, { "key": "job_item_id", @@ -1000,7 +1000,7 @@ }, { "key": "path", - "what": "the self-contained view.html (or the PNG, with --crop)" + "what": "the self-contained view.html (or the PNG, with `--crop`)" }, { "key": "built", @@ -1016,7 +1016,7 @@ }, { "key": "deep_link", - "what": "view.html#element=... when --element-id was given" + "what": "view.html#element=... when `--element-id` was given" }, { "key": "history_items", @@ -1117,7 +1117,7 @@ "what": "one record per PNG (element_id, type, page, box, dpi, path, width, height)" } ], - "note": "One shape whatever matched: a single --element-id is count 1 with one crops[] record; a filter (--type/--page/--all) matching nothing is count 0 with crops []." + "note": "One shape whatever matched: a single `--element-id` is count 1 with one crops[] record; a filter (`--type`/`--page`/`--all`) matching nothing is count 0 with crops []." }, "band": "local read models" } diff --git a/src/ade_cli/auth.py b/src/ade_cli/auth.py index bd4b2c2..1ec53d6 100644 --- a/src/ade_cli/auth.py +++ b/src/ade_cli/auth.py @@ -74,9 +74,9 @@ def login( environment: str | None = typer.Option(None, "--env", help=_ENV_HELP), as_json: bool = JSON_FLAG, ) -> None: - """Ensure the target environment is logged in; --api-key + """Ensure the target environment is logged in; `--api-key` authenticates with a key directly ('-' prompts with hidden input). - Targets --env, else $ADE_ENV, else production — nothing is stored + Targets `--env`, else $ADE_ENV, else production — nothing is stored about the choice.""" home = ade_home() resolved = resolve_target(home, environment, as_json=as_json) @@ -586,7 +586,7 @@ def logout( ), as_json: bool = JSON_FLAG, ) -> None: - """Log out of one environment (the resolved target by default); --all + """Log out of one environment (the resolved target by default); `--all` clears every environment. Idempotent; OAuth refresh tokens are revoked best-effort first.""" home = ade_home() diff --git a/src/ade_cli/extract.py b/src/ade_cli/extract.py index 94701e7..9782c16 100644 --- a/src/ade_cli/extract.py +++ b/src/ade_cli/extract.py @@ -273,7 +273,7 @@ def extract( help=f"Environment to run against: {', '.join(ENVIRONMENTS)} " "(default: $ADE_ENV, then production). The JOB_ITEM_ID form inherits the " "parse item's environment instead — its server-side parse job only " - "exists there — and a conflicting --env is refused.", + "exists there — and a conflicting `--env` is refused.", ), wait: float = typer.Option(600.0, "--wait", help="Poll budget in seconds."), force: bool = typer.Option( diff --git a/src/ade_cli/find.py b/src/ade_cli/find.py index f0dbacd..6c2f61b 100644 --- a/src/ade_cli/find.py +++ b/src/ade_cli/find.py @@ -33,7 +33,7 @@ def find( None, metavar="[JOB_ITEM_ID] [QUERY]", help="Job item id (or unambiguous prefix) to search, then an " - "optional case-insensitive substring QUERY. With --job, the one " + "optional case-insensitive substring QUERY. With `--job`, the one " "allowed positional is the QUERY.", ), job_tokens: list[str] = typer.Option( @@ -61,7 +61,7 @@ def find( as_json: bool = JSON_FLAG, id_only: bool = ID_ONLY_FLAG, ) -> None: - """Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or --job + """Search parsed elements locally: `find JOB_ITEM_ID [QUERY]`, or `--job` (repeatable) for several items; no query lists every element. Ids discovered here are what `view --element-id` deep-links and diff --git a/src/ade_cli/help.py b/src/ade_cli/help.py index 7b9779e..b8ae87e 100644 --- a/src/ade_cli/help.py +++ b/src/ade_cli/help.py @@ -180,8 +180,8 @@ ("stored", "false when the result could not be persisted"), ("store_dir", "absolute path of the job item folder"), ("artifacts", "artifact filenames written there"), - ("markdown", "the parse markdown — only with --include markdown"), - ("elements", "the flat projection — only with --include elements"), + ("markdown", "the parse markdown — only with `--include markdown`"), + ("elements", "the flat projection — only with `--include elements`"), ], }, "extract": { @@ -222,7 +222,7 @@ ("text", "the element's markdown slice"), ], "note": "One record per match, in document order; [] when nothing " - "matched. --id-only prints the element ids alone.", + "matched. `--id-only` prints the element ids alone.", }, "crop": { "shape": "object", @@ -234,21 +234,21 @@ ("crops", "one record per PNG (element_id, type, " "page, box, dpi, path, width, height)"), ], - "note": "One shape whatever matched: a single --element-id is " - "count 1 with one crops[] record; a filter (--type/--page/--all) " + "note": "One shape whatever matched: a single `--element-id` is " + "count 1 with one crops[] record; a filter (`--type`/`--page`/`--all`) " "matching nothing is count 0 with crops [].", }, "view": { "shape": "object", "keys": [ - ("status", "'viewed' ('cropped' with --crop, 'synced' with --sync-viewers)"), + ("status", "'viewed' ('cropped' with `--crop`, 'synced' with `--sync-viewers`)"), ("job_item_id", "the item rendered"), ("kind", "parse | extract"), - ("path", "the self-contained view.html (or the PNG, with --crop)"), + ("path", "the self-contained view.html (or the PNG, with `--crop`)"), ("built", "true when this run rebuilt the artifact"), ("pages_embedded", "pages inlined; the rest load from sidecars"), ("note", "why the render weakened, when it did (else null)"), - ("deep_link", "view.html#element=... when --element-id was given"), + ("deep_link", "view.html#element=... when `--element-id` was given"), ("history_items", "items in the rebuilt sidebar read model"), ("sidebar_sync", "true when sibling viewers build in the background"), ], @@ -265,7 +265,7 @@ ("parse", "extract items: the referenced parse — {job_item_id, " "run_id (the parse generation extracted against), missing}"), ("stale", "extract items: true when the referenced parse was " - "--force re-run after this extraction"), + "`--force` re-run after this extraction"), ("created_at / completed_at", "epoch seconds (null when unknown)"), ], }, @@ -308,7 +308,7 @@ ("cleared", "false when there was nothing stored to clear"), ("revoked", "refresh tokens revoked best-effort"), ("scope", "environment | all"), - ("environment", "the environment cleared (null with --all)"), + ("environment", "the environment cleared (null with `--all`)"), ], }, "version": { @@ -387,7 +387,7 @@ CONVENTIONS = [ ( "--json", - "Every command supports --json: one stable JSON object/array on " + "Every command supports `--json`: one stable JSON object/array on " "stdout (errors and pending payloads follow the same rule). Agents " "should always pass it. Each command's published shape is its " "'result' block below — the full result is always on stdout, never " @@ -395,8 +395,8 @@ ), ( "--id-only", - "parse, extract, and find also take --id-only: just the id(s), one " - "per line, for piping (JOB=$(ade parse -d f.pdf --id-only)). Errors " + "parse, extract, and find also take `--id-only`: just the id(s), one " + "per line, for piping (`JOB=$(ade parse -d f.pdf --id-only)`). Errors " "and hints go to stderr so a captured id is never a sentence.", ), ( @@ -409,7 +409,7 @@ "guarantees", "parse and extract ensure a run exists rather than fire a request: " "an already-done run is served from disk free with an explicit " - "notice (--force consents to a re-bill); a pending run is resumed, " + "notice (`--force` consents to a re-bill); a pending run is resumed, " "never resubmitted; Ctrl-C stops the waiting, not the work.", ), ( @@ -677,7 +677,7 @@ def help_command( """Print the whole-surface command reference in one call: every command and flag, the output convention, each verb's result shape, exit states, and the store layout. The agent bootstrap — run this - (with --json) before anything else. `help TOPIC` prints one + (with `--json`) before anything else. `help TOPIC` prints one conceptual page instead (workflow, output, credentials, errors).""" root = ctx.find_root().command assert isinstance(root, TyperGroup) diff --git a/src/ade_cli/main.py b/src/ade_cli/main.py index b6a4bfa..5d3c5c1 100644 --- a/src/ade_cli/main.py +++ b/src/ade_cli/main.py @@ -133,13 +133,13 @@ def _force_utf8_stdio() -> None: app.command( "login", help="Alias of `ade auth login`: ensure the target environment is " - "logged in; --api-key authenticates with a key directly ('-' prompts " + "logged in; `--api-key` authenticates with a key directly ('-' prompts " "with hidden input).", )(login) app.command( "logout", help="Alias of `ade auth logout`: log out of one environment (the " - "resolved target by default); --all clears every environment.", + "resolved target by default); `--all` clears every environment.", )(logout) app.command()(parse) app.command()(extract) diff --git a/src/ade_cli/output.py b/src/ade_cli/output.py index 72f2bb4..2eda30e 100644 --- a/src/ade_cli/output.py +++ b/src/ade_cli/output.py @@ -33,8 +33,8 @@ False, "--id-only", help="Print only the id(s) this run produced, one per line — the " - "piping mode (JOB=$(ade parse -d f.pdf --id-only)). Takes precedence " - "over --json; errors and hints go to stderr.", + "piping mode (JOB=$(ade parse -d f.pdf `--id-only`)). Takes precedence " + "over `--json`; errors and hints go to stderr.", ) # Machine-readable exit states, shared by every command: pending is a normal diff --git a/src/ade_cli/parse.py b/src/ade_cli/parse.py index 8676f56..b8d49e6 100644 --- a/src/ade_cli/parse.py +++ b/src/ade_cli/parse.py @@ -102,12 +102,12 @@ def parse( ctx: typer.Context, document: Path | None = typer.Option( None, "-d", "--document", exists=True, dir_okay=False, readable=True, - help="Local document file to parse; pass exactly one of -d/--document or --document-url.", + help="Local document file to parse; pass exactly one of -d/`--document` or `--document-url`.", ), document_url: str | None = typer.Option( None, "--document-url", help="Document URL the server fetches (identity is the URL x params; " - "a re-run dedups even if the remote content changed — --force refreshes).", + "a re-run dedups even if the remote content changed — `--force` refreshes).", ), model: str = typer.Option( DEFAULT_MODEL, "--model", help="Parse model registry version." @@ -138,7 +138,7 @@ def parse( "password: always rejected (422) — decrypt PDFs before upload. " "Example: '{\"inline_markdown\": true, \"blocks\": {\"table\": " "{\"format\": \"markdown\"}}}'. " - "Merges with --pages; giving pages in both is an error.", + "Merges with `--pages`; giving pages in both is an error.", ), environment: str | None = typer.Option( None, "--env", diff --git a/src/ade_cli/view.py b/src/ade_cli/view.py index 88a666a..72ed663 100644 --- a/src/ade_cli/view.py +++ b/src/ade_cli/view.py @@ -898,18 +898,18 @@ def view( crop: bool = typer.Option( False, "--crop", - help="With --element-id: render that element's PNG crop instead of " + help="With `--element-id`: render that element's PNG crop instead of " "the HTML artifact.", ), open_browser: bool | None = typer.Option( None, "--open/--no-open", help="Open the result in the browser. Default: open when stdout is " - "a terminal; --json runs and piped output never auto-open.", + "a terminal; `--json` runs and piped output never auto-open.", ), dpi: int | None = typer.Option( None, "--dpi", min=1, - help=f"Page render dpi (default {DEFAULT_DPI}); with --crop, the crop " + help=f"Page render dpi (default {DEFAULT_DPI}); with `--crop`, the crop " f"dpi (default {DEFAULT_CROP_DPI}).", ), pages: str | None = typer.Option( @@ -924,11 +924,11 @@ def view( help="Open via a local server (http://127.0.0.1) instead of file:// " "— browser zoom then covers every viewer natively. Starts the " "server if needed; reuses a running one. It retires itself after " - "30 idle minutes, or immediately with --stop-server.", + "30 idle minutes, or immediately with `--stop-server`.", ), stop_server: bool = typer.Option( False, "--stop-server", - help="Stop the local viewer server (started by --serve) and exit.", + help="Stop the local viewer server (started by `--serve`) and exit.", ), sync_viewers: bool = typer.Option( False, "--sync-viewers", hidden=True, diff --git a/tests/test_help_style.py b/tests/test_help_style.py new file mode 100644 index 0000000..48a0c5d --- /dev/null +++ b/tests/test_help_style.py @@ -0,0 +1,67 @@ +"""Style guard for the help surface: inline code is always backticked. + +A bare ``--flag`` in help prose is ambiguous for agents and renders as +an em dash plus the flag name on the hosted docs, which generate the +CLI reference from ``help --json``. See docs/agents/writing-style.md. + +Scope: every prose field in the help surface. Topic bodies are exempt: +they are pre-formatted terminal layouts (aligned flag columns, literal +example commands) where backticks would corrupt the rendering. Other +inline-code kinds (paths, commands, env vars) stay convention; a test +cannot reliably tell a path from prose, but a long flag is unmistakable. +""" + +from __future__ import annotations + +import json +import re + +import pytest + +# A long flag in prose that is not already inside a backtick span. +CODE_SPAN = re.compile(r"`[^`]*`") +BARE_FLAG = re.compile(r"(? dict: + result = cli.invoke("help", "--json") + assert result.exit_code == 0 + return json.loads(result.stdout) + + +def _prose_strings(reference: dict): + """Yield (locator, text) for every prose string in the help surface.""" + yield "description", reference["description"] + for convention in reference["conventions"]: + yield f"conventions[{convention['name']}]", convention["rule"] + for command in reference["commands"]: + where = f"commands[{command['name']}]" + yield f"{where}.summary", command["summary"] + for argument in command["arguments"]: + yield f"{where}.arguments[{argument['name']}]", argument["help"] + for flag in command["flags"]: + yield f"{where}.flags[{flag['flags']}]", flag["help"] + for key in command.get("result", {}).get("keys", []): + yield f"{where}.result[{key['key']}]", key["what"] + note = command.get("result", {}).get("note") + if note: + yield f"{where}.result.note", note + # topics[].body is deliberately absent: pre-formatted terminal text. + for state in reference["exit_states"]: + yield f"exit_states[{state['name']}]", state["meaning"] + for entry in reference["store"]["layout"]: + yield f"store[{entry['path']}]", entry["what"] + + +def test_flags_in_help_prose_are_backticked(reference): + """Every --flag mentioned in help prose is wrapped in backticks.""" + offenders = [] + for locator, text in _prose_strings(reference): + stripped = CODE_SPAN.sub("", text) + for match in BARE_FLAG.findall(stripped): + offenders.append(f"{locator}: bare {match}") + assert not offenders, ( + "Bare flags in help prose; wrap them in backticks " + "(docs/agents/writing-style.md):\n" + "\n".join(offenders) + )