Data Model: Common Docs query — CLI retrieval index
Module layering (packaging-critical)
scripts/ is not shipped in the wheel (pyproject.toml packages only src/); the installed spec-kitty CLI cannot import scripts.. Therefore the schema + query surface the CLI needs live in a packaged module, and the generator (build-tooling) imports down into it:
| Symbol | Home | Imported by |
|---|---|---|
Anchor, DocsQueryEntry, render_index, parse_index, compare_index, IndexDrift, DocsIndexStore, DEFAULT_INDEX_PATH | src/specify_cli/docs/index_model.py (packaged) | the CLI (src→src), the generator (scripts→src) |
generate_index, scan_headings, resolve_title, resolve_abstract, slug_for_headings, run_generate_and_compare, __main__ | scripts/docs/docs_index.py (build-tooling) | the freshness gate (scripts→scripts) |
Legal direction only: scripts → src. Never src → scripts (would ModuleNotFoundError in the installed CLI). The CLI (WP03) imports only specify_cli.docs.index_model — never scripts.
Entity: DocsQueryEntry (one per docs/*/.md page)
A new frozen dataclass in src/specify_cli/docs/index_model.py (packaged). Do NOT reuse or widen PageInventoryEntry (C-001), and do NOT place it in scripts/ (unpackaged).
| Field | Type | Source / rule | Notes |
|---|---|---|---|
path | str | repo-root-relative POSIX path of the page | Primary key; index sorted by this (NFR-001). |
title | str | frontmatter title → first # H1 → path stem | Total precedence; never empty (path stem is the floor). |
divio_type | str | frontmatter type: coerced to DivioType | Reuses DivioType StrEnum (`tutorial\ |
anchors | list[Anchor] | scan body ##/### headings | Empty list allowed (page with no subheadings). |
abstract | str | frontmatter description → first non-heading paragraph → "" | Empty allowed (ADR/changelog exemption). |
Value object: Anchor
| Field | Type | Rule |
|---|---|---|
slug | str | canonical slugify(text) + ordinal suffix (-2, -3, …) for duplicates within a page |
text | str | the heading's rendered text (stripped of leading #s and surrounding whitespace) |
level | int | 2 or 3 (## / ###) |
Invariants
(NFR-001).
slugvalues within a single page are unique (ordinal disambiguation guarantees this).- Anchors preserve document order.
- Slug generation is pure and deterministic → the whole index is byte-stable for an unchanged tree
Generated artifact: docs/development/3-2-docs-retrieval-index.yaml
inventory lockfile convention).
anchors as a list of {slug, text, level} maps.
- Git-tracked, generated by
scripts/docs/docs_index.py, byte-stable. - Leading
# GENERATED — do not edit by hand; run <generator> to refresh.header comment (mirrors the - Top-level: a
pages:list of entries sorted bypath; each entry serializes the fields above with - The page-inventory (
3-2-page-inventory.yaml) is a separate file, untouched (C-001).
Schema sketch
# GENERATED — do not edit by hand.
pages:
- path: docs/architecture/execution-lanes.md
title: Execution lanes
divio_type: explanation
abstract: How Spec Kitty allocates worktrees per computed lane.
anchors:
- {slug: overview, text: Overview, level: 2}
- {slug: lane-allocation, text: Lane allocation, level: 2}
In-memory query model (DocsIndexStore)
Lives in the packaged src/specify_cli/docs/index_model.py (imported by the CLI).
An empty/whitespace term is a validation error (the CLI requires a term argument).
anchor list, so the caller sees which heading matched.
- Loaded once from the generated file at CLI entry (
DocsIndexStore.load(index_path)). - Holds the parsed
list[DocsQueryEntry]. query(term, *, divio_type=None, section=None) -> list[DocsQueryEntry]:- term (case-insensitive substring) matches
titleOR anyanchors[].text/slugORabstract. - divio_type (optional) filters to entries whose
divio_typeequals the givenDivioType. - section (optional) filters to entries containing an anchor whose
slugequals the value. - Results preserve index (path-sorted) order.
- For each matched entry the CLI emits only the matching
anchorsin the result (FR-003), not the full
No persistence / migration
No database, no state file beyond the generated index, no migration. The index is a build artifact committed to the repo, refreshed by its generator and enforced by its freshness gate.