Contracts

cli-contract.md

CLI Contract: spec-kitty docs query

Command

spec-kitty docs query <TERM> [--json] [--divio-type <TYPE>] [--section <ANCHOR-SLUG>]

Registered as a docs Typer sub-app (src/specify_cli/cli/commands/docs.py), added next to glossary in src/specify_cli/cli/commands/__init__.py. The command imports DocsQueryEntry / DocsIndexStore from the packaged specify_cli.docs.index_model (src→src) — it MUST NOT import scripts.* (unpackaged; would fail in the installed CLI).

Arguments & options

NameKindRequiredBehavior
TERMpositional stryesCase-insensitive substring matched against each page's title, any anchor text/slug, and abstract. Empty/whitespace → usage error (exit 2).
--jsonflagnoEmit machine-readable JSON (see shape below) via print(json.dumps(...)). Without it, render a Rich table for humans.
--divio-typestrnoRestrict to pages of this Divio type. MUST validate against DivioType (`tutorial\
--sectionstrnoRestrict to pages containing an anchor whose slug equals this value.

JSON result shape (--json)

A JSON array (possibly empty). Each element:

{
  "path": "docs/architecture/execution-lanes.md",
  "title": "Execution lanes",
  "divio_type": "explanation",
  "abstract": "How Spec Kitty allocates worktrees per computed lane.",
  "anchors": [
    {"slug": "lane-allocation", "text": "Lane allocation", "level": 2}
  ]
}

order. When the match was on title/abstract only (no anchor matched) anchors MAY be empty.

  • anchors contains only the anchors that MATCHED the term/--section filter (FR-003), in document
  • Keys are always present; abstract may be "".

Behavioral contract

#GivenWhenThen
1index has a page matching TERMdocs query "<term>" --jsonthat page appears with path, title, matching anchors, abstract, divio_type; exit 0
2no page matchesdocs query "<term>" --jsonprints []; exit 0 (NOT an error)
3--divio-type referencematching pages exist of other typesonly reference pages returned
4--section <slug>some pages contain that anchoronly those pages returned
5invalid --divio-type valueusage error, exit 2, no traceback
6no docs/ tree / index file missingdocs query "x"clear error message to stderr, non-zero exit; no Python traceback
7human mode (no --json)match existsRich table with path/title/type/matching-anchors; NO Rich markup leaks into piped output

Non-goals (contract-level)

  • No HTTP/REST/GraphQL surface (C-002).
  • No full-text body search — matching is over title/anchor/abstract only (C-003).
  • Anchors are source-heading slugs, not guaranteed byte-identical to rendered DocFX fragments (C-005).

index-file-contract.md

Artifact Contract: docs retrieval index + freshness gate

The generated index file

ONLY this, src→src):

from src, scripts→src):

rglob, slugify+ordinal)

refreshes the file, --strict exits non-zero on drift.

excludes scripts, so an installed-CLI import of scripts.* would ModuleNotFoundError). A test MUST assert the CLI-facing symbols (DocsQueryEntry, Anchor, DocsIndexStore) import from specify_cli.docs.index_model.

regenerations over an unchanged tree produce byte-identical output.

  • Path: docs/development/3-2-docs-retrieval-index.yaml (new, git-tracked).
  • Packaged modelsrc/specify_cli/docs/index_model.py (shipped in the wheel; the CLI imports
  • Anchor, DocsQueryEntry (frozen dataclasses)
  • render_index(entries) -> str (deterministic, byte-stable string)
  • parse_index(text) -> list[DocsQueryEntry]
  • compare_index(committed, regenerated) -> IndexDrift (mirrors InventoryDrift)
  • DocsIndexStore.load(path) + .query(...)
  • DEFAULT_INDEX_PATH
  • Generator (build-tooling)scripts/docs/docs_index.py (NOT shipped; imports the model DOWN
  • generate_index(docs_root) -> list[DocsQueryEntry] (uses parse_frontmatter, DivioType, sorted
  • scan_headings, resolve_title, resolve_abstract, slug_for_headings helpers
  • run_generate_and_compare(..., write: bool, strict: bool)__main__ entrypoint; --write
  • Import-direction invariant: scripts → src is legal; src → scripts is forbidden (the wheel
  • Determinism (NFR-001): entries sorted by path; anchors in document order; slugs pure. Two
  • Header: a # GENERATED — do not edit by hand. leading comment (inventory-lockfile convention).

C-001 invariants (page-inventory untouched)

The following MUST be byte-for-byte unchanged by this mission:

  • docs/development/3-2-page-inventory.yaml
  • scripts/docs/_inventory.py::PageInventoryEntry (schema, _REQUIRED_KEYS)
  • scripts/docs/inventory_lockfile.py::render_lockfile
  • scripts/docs/check_docs_freshness.py::_check_inventory_lockfile_drift
  • tests/docs/test_inventory_path_stable.py, tests/docs/test_bulk_ref_rewrite.py::test_inventory_lockfile_untouched

A regression test SHOULD assert the docs-index generator does not import/mutate PageInventoryEntry.

Freshness gate

index in-memory, compares to the committed file, emits an error-severity finding on drift.

_check_inventory_lockfile_drift. Rule id e.g. DOCS-INDEX-DRIFT.

an explicit step only if the aggregate does not.

  • New checker: scripts/docs/check_docs_freshness.py::_check_docs_index_drift — regenerates the
  • Registration: folded into the aggregate ruler list alongside (not replacing)
  • CI: .github/workflows/docs-freshness.yml — verify the aggregate call covers the new checker; add
  • Behavioral contract:
GivenWhenThen
committed index matches the treecheck_docs_freshness --cinew checker passes (exit 0 contribution)
a page's heading/description changed but index not regeneratedcheck_docs_freshness --ciDOCS-INDEX-DRIFT error, aggregate exits non-zero
index regenerated + committedcheck_docs_freshness --cigreen again

Reuse (C-004)

Import from existing scripts/docs/ surfaces — do not fork: parse_frontmatter, the sorted rglob("*.md") walk, DivioType, the InventoryDrift/compare diff shape, and slugify (+ ordinal-dedup pattern) from generate_kitty_specs_docs.py.