Contracts

build_cli_reference.md

Contract: scripts/docs/build_cli_reference.py

Purpose: Implement FR-007 / FR-008. Build the 3.2 CLI reference markdown from the live Typer app, capturing every visible command path's --help output.

Inputs

  • Environment: SPEC_KITTY_ENABLE_SAAS_SYNC=1 and SPEC_KITTY_NO_UPGRADE_CHECK=1 MUST be set in the process environment before specify_cli.app is imported. The script enforces this by setting them in os.environ at module top before any other import.
  • --output PATH (default docs/reference/cli-commands.md) — destination markdown file.
  • --agent-output PATH (default docs/reference/agent-subcommands.md) — destination for agent-rooted subtree (kept as a separate page per repo convention).
  • --include-hidden flag — append an internal appendix listing the 5 hidden paths.
  • --mode {generated, hybrid, hand} (default hybrid) — controls the generated-block delimiter behavior:
  • generated: the entire body is auto-generated; no hand-authored prose allowed between <!-- BEGIN GENERATED --> and <!-- END GENERATED -->.
  • hybrid (plan default): the generated block is embedded; hand-authored prose lives outside the block and is preserved across runs.
  • hand: the script only writes the deprecation/internal classification table; everything else is hand-authored.
  • --dry-run flag — print the diff without writing.

Outputs

  • docs/reference/cli-commands.md rewritten in place (or printed to stdout under --dry-run).
  • docs/reference/agent-subcommands.md rewritten in place (same gating).
  • stderr: a short summary table — visible/hidden/deprecated counts, generated lines, hand-authored lines preserved.

Exit Codes

CodeMeaning
0Wrote (or would have written) successfully; output matched the expected freshness rules.
1The live tree contains visible paths the generator could not classify; output not written.
2Input error (missing dependencies, malformed CLI app, pyproject.toml not found).
3Environmental setup error (SPEC_KITTY_ENABLE_SAAS_SYNC not set, git status shows uncommitted writes in target files).

Guarantees

  • Read-only on the Typer app. The script imports specify_cli.app but never patches or rebinds command objects.
  • Subprocess-isolated per-path help capture. Each --help is collected via subprocess.run(["uv", "run", "spec-kitty", *path, "--help"], …) with stdout captured. The walker prefers group.name then group.typer_instance.info.name per cli-audit-3-2.md.
  • Idempotent. Re-running on a tree with no changes leaves the output byte-identical.
  • Refuses to write if the target file has uncommitted edits already in the working tree (avoids stomping a reviewer's in-progress prose). Override with --force for ad-hoc local runs.

Non-guarantees

  • The script does NOT decide the hand-vs-generator policy. That is decision 01KS4KTM69EG2KVX5MQ54FQ939. The script honours whichever --mode is passed.
  • The script does NOT modify Typer command code, command files, or docs/toc.yml. Any help-text discrepancy is logged as a MetaIssue row prompt on stderr, not silently fixed.

Test fixtures

  • tests/docs/fixtures/sample_cli_reference.md — the expected output for a synthetic Typer app fixture (a tiny app used in unit tests so the test does not depend on the real CLI).
  • Integration test (smoke) imports the real specify_cli.app with the env flags set and asserts: visible count ≥ 192, deprecated count == 2, hidden count == 5 (current snapshot from cli-audit-3-2.md; test tolerates ±10% with explicit log so an intentional command addition is visible).

Error taxonomy (exemplar messages)

BUILD-ENV-MISSING-SAAS-SYNC
  SPEC_KITTY_ENABLE_SAAS_SYNC=1 must be set before import. tracker/issue-search will be missing without it.

BUILD-UNCLASSIFIED-VISIBLE-PATH spec-kitty foo bar
  Visible command with no help summary and no test reference; classify in cli-audit meta-issues and re-run.

BUILD-TARGET-DIRTY  docs/reference/cli-commands.md
  Target has uncommitted edits in the working tree. Stash, commit, or pass --force.

check_cli_reference_freshness.md

Contract: scripts/docs/check_cli_reference_freshness.py

Purpose: Implement FR-020 / NFR-001. Detect drift between the live Typer tree and the committed CLI reference.

Inputs

  • Environment: SPEC_KITTY_ENABLE_SAAS_SYNC=1 and SPEC_KITTY_NO_UPGRADE_CHECK=1 set before import.
  • --reference PATH (default docs/reference/cli-commands.md).
  • --agent-reference PATH (default docs/reference/agent-subcommands.md).
  • --report PATH (optional) — JSON report slice.
  • --ci flag — plain-text output for CI annotations.
  • --strict-mode flag — also fail when an entry exists in the reference but the matching visible path's help_summary has drifted from what the reference recorded (HELP-DRIFT); off by default to allow hand-authored prose.

Outputs

  • stdout: findings table; counts by rule.
  • optional JSON report.

Rules

rule_idSeverityCondition
REF-MISSINGerrorA visible command path is missing from the reference.
REF-EXTRAerrorA reference entry names a command path not in the live tree.
REF-DEPRECATED-UNCLASSIFIEDerrorA deprecated/removed visible command path is not classified as such in the reference.
REF-INTERNAL-LEAKerrorA path whose live help_summary starts with Internal - appears in the user-facing reference without an "internal" classification banner.
REF-SAAS-SYNC-OFFerrorSPEC_KITTY_ENABLE_SAAS_SYNC was not set, so tracker/issue-search paths could not be evaluated; refuse to declare clean.
HELP-DRIFTwarning (error in --strict-mode)The reference's recorded summary differs from the live help_summary.
REF-HIDDEN-LEAKerrorA hidden path appears in the user-facing reference (allowed only in the --include-hidden appendix).

Exit Codes

CodeMeaning
0No errors.
1One or more errors.
2Input error (missing reference file).
3Environmental setup error (SaaS sync off, CLI fails to import).

Guarantees

  • Read-only.
  • Deterministic.
  • Discovers visible paths the same way build_cli_reference.py does (_typer_walker.py is shared).

Non-guarantees

  • Does not enforce documentation prose quality. Reviewer judgement remains the gate for tone, examples, and clarity.
  • Does not validate that linked tests exist or pass; that is the architectural test's job.

Test fixtures

  • tests/docs/fixtures/sample_cli_reference.md (clean) — exit code 0.
  • tests/docs/fixtures/sample_cli_reference_missing.mdREF-MISSING count == 1; exit 1.
  • tests/docs/fixtures/sample_cli_reference_extra.mdREF-EXTRA count == 1; exit 1.
  • tests/docs/fixtures/sample_cli_reference_no_saas.md — env flag off in subtest; REF-SAAS-SYNC-OFF; exit 3.

check_docs_freshness.md

Contract: scripts/docs/check_docs_freshness.py

Purpose: Implement FR-020 / FR-021. Orchestrate every docs-freshness check into a single CI-friendly report.

Inputs

  • Environment: same as check_cli_reference_freshness.py (SPEC_KITTY_ENABLE_SAAS_SYNC=1, SPEC_KITTY_NO_UPGRADE_CHECK=1).
  • --inventory PATH, --docs-root PATH, --reference PATH, --agent-reference PATH — pass-through to the underlying checks.
  • --link-check {none,spot,full} (default spot) — none skips link health; spot checks 20 random current-tagged pages plus the CLI reference for http(s):// links and runs HEAD requests; full checks every external link.
  • --report PATH (optional) — single JSON FreshnessReport.
  • --ci flag — plain text output.
  • --strict-mode flag — pass-through to underlying CLI freshness checker.

Sub-checks

1. version_leakage_check.py — gathers LEAK- findings. 2. check_cli_reference_freshness.py — gathers REF- and HELP-* findings. 3. Link health (per --link-check). 4. Page-inventory completeness — every .md under docs/ is present in the manifest or explicitly excluded by glob.

Outputs

  • Aggregated FreshnessReport. Errors from any sub-check propagate. Warnings are reported but do not fail.

Exit Codes

CodeMeaning
0No errors across any sub-check.
1One or more sub-checks reported errors.
2Input error (missing inventory or reference).
3Environmental setup error.

Guarantees

  • Single point of entry for the publication gate (FR-021).
  • Each sub-check runs in isolation; one sub-check's input error does not skip the others.

Non-guarantees

  • Does not build the docs site; that remains a separate workflow step in CI.
  • Does not write to disk except for the optional --report JSON.

CI wiring (informational)

Tasks-phase WP F3 wires this into .github/workflows/ci-quality.yml (or the current equivalent) as:

- name: Docs freshness
  run: |
    SPEC_KITTY_ENABLE_SAAS_SYNC=1 SPEC_KITTY_NO_UPGRADE_CHECK=1 \
      uv run scripts/docs/check_docs_freshness.py --ci --report freshness.json

The CI step uploads freshness.json as an artifact for the publication checklist evidence.

Test fixtures

  • tests/docs/test_check_docs_freshness.py covers:
  • Happy path → exit 0.
  • One leak + one reference miss → aggregated exit 1.
  • Missing inventory → exit 2.
  • SaaS sync off → exit 3.

version_leakage_check.md

Contract: scripts/docs/version_leakage_check.py

Purpose: Enforce FR-005 / NFR-002. Detect docs pages tagged current that link to archival pages without an explicit archive or migration banner, and detect pages whose frontmatter version_tag disagrees with the page-inventory manifest.

Inputs

  • --inventory PATH (default docs/development/3-2-page-inventory.yaml) — PageInventoryEntry rows.
  • --docs-root PATH (default docs/) — root directory to scan.
  • --banner-regex PATTERN (default r"^>\s*(?:Archive notice|Migration note)\b") — pattern that must appear within the first 20 non-empty lines of an archival or migration page.
  • --report PATH (optional) — write a JSON FreshnessReport slice to this path.
  • --ci flag — suppress rich output; emit plain-text lines suitable for CI annotations.

Environment: no SaaS access required. No mutation of input files at any time.

Outputs

  • stdout: rich table (interactive) or plain text (CI) listing findings. Findings use rule_id prefixes:
  • LEAK-CURRENT-LINKS-ARCHIVAL — a current page links to an archival path without a migration banner.
  • LEAK-MISSING-BANNER — an archival or migration page is missing the banner.
  • LEAK-FRONTMATTER-MISMATCH — the page's frontmatter version_tag disagrees with the manifest row.
  • LEAK-MISSING-INVENTORY — a markdown file under docs/ is not in the manifest.
  • LEAK-MISSING-FILE — a manifest row points at a non-existent file.
  • optional JSON report (--report).

Exit Codes

CodeMeaning
0No findings of severity error. Warnings allowed.
1One or more error findings.
2Input error (missing inventory file, malformed YAML).
3Environmental setup error (cannot resolve docs-root, permission denied).

Guarantees

  • Read-only. The script never writes to docs/ or to the inventory.
  • Deterministic. Given the same inputs the output is byte-identical.
  • O(N) over the number of pages × number of links per page. Designed for repos with <5k pages and <50 links per page.

Non-guarantees

  • No network access. Link health (HTTP 200 vs 404) is not checked here; see check_docs_freshness.py for the orchestrated link spot-check.
  • No language-aware link normalization (anchor parsing is naïve: text strips #anchor before classification).

Test fixtures

  • tests/docs/fixtures/clean_inventory.yaml + tests/docs/fixtures/sample_pages/ — happy path; exit code 0.
  • tests/docs/fixtures/dirty_inventory.yaml — one of every finding rule; exit code 1; findings count == 5.
  • tests/docs/fixtures/missing_inventory.yaml — input error; exit code 2.

Error taxonomy (exemplar messages)

LEAK-CURRENT-LINKS-ARCHIVAL  docs/how-to/install-macos.md
  -> links to docs/1x/legacy-install.md without migration banner
  fix: add archive callout above the link, or update target to docs/migration/from-2x-to-3-2.md

LEAK-MISSING-BANNER  docs/1x/index.md
  -> archival page missing banner matching /^>\s*Archive notice\b/
  fix: prepend "> Archive notice: This page documents Spec Kitty 1.x and is preserved for historical context." to the page.

LEAK-FRONTMATTER-MISMATCH  docs/explanation/mission-model.md
  -> frontmatter says version_tag=supported, manifest says version_tag=current
  fix: reconcile by updating frontmatter or by editing 3-2-page-inventory.yaml.