Research: Common Docs query — CLI retrieval index

Consolidated from the pre-plan adversarial squad (architect-alphonso + python-pedro, convergent, code-cited). Every decision below is grounded in an existing repo surface; no NEEDS CLARIFICATION remain.

Decision 1 — Sibling artifact, NOT enrichment of the page-inventory

(docs/development/3-2-docs-retrieval-index.yaml) built by a new module (scripts/docs/docs_index.py) with its own DocsQueryEntry schema.

dataclass with a _REQUIRED_KEYS frozenset, imported by 8 tooling modules + 6 test files, rendered byte-stably by render_lockfile (inventory_lockfile.py:227) and guarded by a now-blocking _check_inventory_lockfile_drift (check_docs_freshness.py:681). Widening it changes every committed inventory row → instant drift red, and forces edits to test_inventory_path_stable.py and test_inventory_lockfile_untouched — the exact C-001 consumers. The sibling holds C-001 by construction.

blocking gate). A brand-new framework unrelated to the inventory (rejected — violates C-004; the inventory generator is the proven deterministic pattern to mirror).

  • Decision: The retrieval index is a new, separate generated file
  • Rationale: PageInventoryEntry (scripts/docs/_inventory.py:93) is a frozen 6-field
  • Alternatives considered: Enrich PageInventoryEntry in place (rejected — breaks C-001, reds the

Decision 2 — Reuse the inventory generator machinery (C-004)

ruamel-safe docs-page frontmatter extractor), the sorted rglob("*.md") walk, the DivioType StrEnum (_inventory.py:83, sourced from the frontmatter type: key per inventory_lockfile.py:68), and the InventoryDrift / compare_inventories diff shape.

the right surface (NOT src/specify_cli/frontmatter.py, which is the WP-file manager).

  • Decision: Import and reuse parse_frontmatter (scripts/docs/_inventory.py:38, the canonical
  • Rationale: These are the canonical, already-tested docs-page primitives. parse_frontmatter is
  • Alternatives considered: A second frontmatter parser (rejected — DIRECTIVE_044).

Decision 3 — Parallel freshness gate, inventory ruler untouched

aggregate as a new error-severity ruler; leave _check_inventory_lockfile_drift byte-for-byte unchanged.

aggregate (auto-covering the new gate) or needs an explicit step.

  • Decision: Add _check_docs_index_drift to check_docs_freshness.py and register it in the
  • Rationale: Mirrors the proven drift pattern; C-001 keeps the inventory gate untouched.
  • Open verification (Phase 2): confirm whether .github/workflows/docs-freshness.yml invokes the

Decision 4 — Mirror the glossary CLI shape

registered via app.add_typer(docs_module.app, name="docs", ...) next to glossary (cli/commands/__init__.py:208). JSON path uses print(json.dumps(output, indent=2)) (not Rich console); empty result prints "[]" and returns (exit 0); human path uses a Rich Table.

Acceptance Scenario 2.

consumers). A new top-level command instead of a sub-app (rejected — inconsistent with glossary).

  • Decision: New src/specify_cli/cli/commands/docs.py Typer sub-app with a query command,
  • Rationale: glossary.py:292-363 is the exact proven shape; empty→[]/exit-0 satisfies
  • Alternatives considered: Rich console.print_json (rejected — markup/formatting risk for machine

Decision 5 — Anchor authority: canonical slugify + ordinal dedup, NOT DocFX-exact

(generate_kitty_specs_docs.py:193, already import-shared by glossary_linker.py) plus ordinal disambiguation (the assign_anchor_ids pattern) for duplicate headings. Anchors are deterministic source-heading slugs; byte-identical fidelity to the rendered DocFX site is an explicit non-goal (C-005).

rendered by the Python slugify. Reverse-engineering DocFX's JS slugger is disproportionate; the index's job is to point an agent at the correct source heading deterministically, which the canonical slugify does. Reusing it (not forking) satisfies DIRECTIVE_044 and keeps NFR-001 byte-stability.

a fresh slugger (rejected — canonical-sources violation).

  • Decision: Compute heading anchors with the canonical slugify
  • Rationale: The docs/ tree is a DocFX site (toc.yml/docfx.json, seo_postprocess.py), not
  • Alternatives considered: Replicate DocFX exactly (rejected — scope/maintenance cost, C-005). Write

Decision 6 — Title precedence and abstract fallback

frontmatter description else first non-heading paragraph else empty. Both are total (never crash).

description (the _EXCLUDE_PREFIXES set in description_length_check.py) are included with an empty abstract — consistent with that gate's exemptions.

helpers (no existing extractor); trivially unit-testable.

  • Decision: title = frontmatter title else first # H1 else path stem. abstract =
  • Rationale: NFR-001 needs a deterministic, total precedence. ADR/changelog pages with no
  • New code: the first-non-heading-paragraph extractor and the ##/### heading scan are new pure

Performance (NFR-002)

term/divio-type/section over that structure. No per-query filesystem walk of doc bodies.

makes full-tree query trivially < 1s.

  • Decision: Load the generated index file once at command entry into an in-memory structure; filter
  • Rationale: Mirrors the glossary in-memory-cache filter (glossary.py:160); a pre-generated index