From 2e7e8e8a34b136c89fe78ae539f334d9be8be706 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 25 Mar 2026 23:18:52 +0100 Subject: [PATCH 1/8] Fix review findings --- .gitignore | 8 ++ docs/Gemfile.lock | 26 +++-- docs/bundles/backlog/overview.md | 106 ++++++++++++++++++ docs/bundles/code-review/overview.md | 58 ++++++++++ docs/bundles/codebase/overview.md | 74 ++++++++++++ docs/bundles/govern/overview.md | 45 ++++++++ docs/bundles/project/overview.md | 94 ++++++++++++++++ docs/bundles/spec/overview.md | 75 +++++++++++++ docs/index.md | 16 +-- openspec/specs/bundle-overview-pages/spec.md | 37 ++++++ tests/unit/docs/test_docs_review.py | 4 + tests/unit/test_modules_docs_site_contract.py | 3 + 12 files changed, 526 insertions(+), 20 deletions(-) create mode 100644 docs/bundles/backlog/overview.md create mode 100644 docs/bundles/code-review/overview.md create mode 100644 docs/bundles/codebase/overview.md create mode 100644 docs/bundles/govern/overview.md create mode 100644 docs/bundles/project/overview.md create mode 100644 docs/bundles/spec/overview.md create mode 100644 openspec/specs/bundle-overview-pages/spec.md diff --git a/.gitignore b/.gitignore index 2974ef4..6e50d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,11 @@ logs/ .vibe/skills/openspec-*/ !.vibe/skills/openspec-workflows/ + +# Jekyll docs — local Bundler state and build output +docs/.bundle/ +docs/_site/ + +# local bundle installs (bundle config path vendor/bundle) +docs/vendor/ + diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 972dd41..582f1a5 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,21 +1,21 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) base64 (0.3.0) colorator (1.1.0) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.6) csv (3.3.5) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) eventmachine (1.2.7) - ffi (1.17.2) + ffi (1.17.3) forwardable-extended (2.6.0) google-protobuf (3.25.8-x86_64-linux) - http_parser.rb (0.8.0) - i18n (1.14.7) + http_parser.rb (0.8.1) + i18n (1.14.8) concurrent-ruby (~> 1.0) jekyll (4.4.1) addressable (~> 2.4) @@ -50,15 +50,17 @@ GEM jekyll (>= 3.7, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - json (2.17.1.2) - kramdown (2.5.1) - rexml (>= 3.3.9) + json (2.19.3) + kramdown (2.5.2) + rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.9.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) mercenary (0.4.0) minima (2.5.2) jekyll (>= 3.5, < 5.0) @@ -72,7 +74,7 @@ GEM rb-inotify (0.11.1) ffi (~> 1.0) rexml (3.4.4) - rouge (4.6.1) + rouge (4.7.0) safe_yaml (1.0.5) sass-embedded (1.69.5) google-protobuf (~> 3.23) @@ -80,7 +82,7 @@ GEM terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) unicode-display_width (2.6.0) - webrick (1.9.1) + webrick (1.9.2) PLATFORMS x86_64-linux diff --git a/docs/bundles/backlog/overview.md b/docs/bundles/backlog/overview.md new file mode 100644 index 0000000..ff99f4f --- /dev/null +++ b/docs/bundles/backlog/overview.md @@ -0,0 +1,106 @@ +--- +layout: default +title: Backlog bundle overview +nav_order: 2 +permalink: /bundles/backlog/overview/ +--- + +# Backlog bundle overview + +The **Backlog** bundle (`nold-ai/specfact-backlog`) connects SpecFact to external backlog tools (GitHub Issues, Azure DevOps, and other adapters), adds ceremony-oriented workflows, dependency analysis, delta tracking, and deterministic backlog policy checks. + +## Prerequisites + +- [SpecFact CLI](https://docs.specfact.io) installed +- Bundle installed: `specfact module install nold-ai/specfact-backlog` (or include it via `specfact init --profile …`) +- For provider access: configure tokens or use `specfact backlog auth` where your adapter requires it + +## Command surface + +After installation, `specfact backlog --help` lists the backlog command group. The backlog category also mounts **policy** as `specfact backlog policy` (see below). + +### Ceremony (`specfact backlog ceremony`) + +| Command | Purpose | +|--------|---------| +| `standup` | Alias for daily standup-style views (`backlog daily`) | +| `refinement` | Alias for AI-assisted refinement (`backlog refine`) | +| `planning` | Delegates to sprint-summary style flows when available | +| `flow` | Kanban-style flow views when available | +| `pi-summary` | PI summary views when available | + +### Core workflows + +| Command | Purpose | +|--------|---------| +| `daily` | Daily standup table: filtered items, optional interactive mode, summarize prompts | +| `refine` | AI-assisted template-driven refinement (preview/write, filters, DoR checks) | +| `add` | Create backlog items with hierarchy and Definition-of-Ready validation | +| `sync` | Sync backlog graph with stored baseline and export delta outputs | +| `verify-readiness` | Verify release readiness for selected backlog items | +| `analyze-deps` | Analyze backlog dependencies for a project | +| `diff` | Show changes since baseline sync | +| `promote` | Validate promotion impact and print promotion intent | +| `init-config` | Scaffold `.specfact/backlog-config.yaml` structure | +| `map-fields` | Interactive mapping of ADO fields to canonical names | + +### Delta (`specfact backlog delta`) + +| Command | Purpose | +|--------|---------| +| `status` | Compare current graph to baseline; show delta summary | +| `impact` | Downstream dependency impact for an item | +| `cost-estimate` | Rough effort points from delta volume | +| `rollback-analysis` | Rollback risk from current delta | + +### Policy (`specfact backlog policy`) + +Deterministic policy validation against backlog snapshots (bundled with this package; not core CLI-owned). + +| Command | Purpose | +|--------|---------| +| `init` | Scaffold `.specfact/policy.yaml` from a template | +| `validate` | Run policy checks (JSON/Markdown/both) | +| `suggest` | Patch-ready suggestions (no automatic writes) | + +See [Policy engine](policy-engine.md) for details. + +### Other + +| Command | Purpose | +|--------|---------| +| `auth` | Authenticate backlog providers | + +## Bundle-owned prompts and templates + +Refinement and ceremony flows **emit prompts and instructions** for your IDE copilot. Those assets ship with the backlog bundle (and related payloads); they are **not** maintained as core CLI-owned files. Install or refresh IDE resources with `specfact init ide` (and your team’s bundle publishing workflow) so the CLI and bundles stay aligned. + +## Quick examples + +Validate the exact flags for your adapter: + +```bash +specfact backlog daily --help +specfact backlog refine --help +specfact backlog delta status --help +specfact backlog policy validate --help +``` + +GitHub refinement preview (typical entry point): + +```bash +specfact backlog refine github --preview --labels feature +``` + +Daily standup scope: + +```bash +specfact backlog daily github --state open --limit 20 +``` + +## Deep dives + +- [Refinement](refinement/) +- [Dependency analysis](dependency-analysis/) +- [Delta](delta/) +- [Policy engine](policy-engine/) diff --git a/docs/bundles/code-review/overview.md b/docs/bundles/code-review/overview.md new file mode 100644 index 0000000..8aa8cb6 --- /dev/null +++ b/docs/bundles/code-review/overview.md @@ -0,0 +1,58 @@ +--- +layout: default +title: Code Review bundle overview +nav_order: 2 +permalink: /bundles/code-review/overview/ +--- + +# Code Review bundle overview + +The **Code Review** bundle (`nold-ai/specfact-code-review`) extends the shared **`specfact code`** command group with **`review`** workflows: governed review runs, **reward ledger** history, and **house-rules** skill management. + +Use it together with the [Codebase](../codebase/overview/) bundle (`import`, `analyze`, `drift`, `validate`, `repro`) on the same `code` surface. + +## Prerequisites + +- `specfact module install nold-ai/specfact-code-review` +- Optional tool installs (Ruff, Radon, Semgrep, Pyright, etc.) as described in command help + +## `specfact code review` — nested commands + +| Command | Purpose | +|--------|---------| +| `run` | Execute a governed review (scope, JSON output, `--fix`, TDD gate, etc.) | +| `ledger` | Inspect and update review reward history | +| `rules` | Manage the house-rules skill (`show`, `init`, `update`) | + +### `ledger` subcommands + +| Subcommand | Purpose | +|------------|---------| +| `update` | Update ledger entries | +| `status` | Show ledger status | +| `reset` | Reset ledger state | + +### `rules` subcommands + +| Subcommand | Purpose | +|------------|---------| +| `show` | Show current rules configuration | +| `init` | Initialize rules/skill assets | +| `update` | Update rules content | + +## Bundle-owned skills and policy packs + +House rules and review payloads ship **inside the bundle** (for example Semgrep packs and skill metadata). They are **not** core CLI-owned resources. Install or refresh IDE-side assets with `specfact init ide` after upgrading the bundle. + +## Quick examples + +```bash +specfact code review run --help +specfact code review ledger status --help +specfact code review rules show --help +``` + +## See also + +- [Code review module](../../modules/code-review.md) +- [Codebase bundle overview](../codebase/overview/) — import, drift, validation, repro diff --git a/docs/bundles/codebase/overview.md b/docs/bundles/codebase/overview.md new file mode 100644 index 0000000..3663eb9 --- /dev/null +++ b/docs/bundles/codebase/overview.md @@ -0,0 +1,74 @@ +--- +layout: default +title: Codebase bundle overview +nav_order: 2 +permalink: /bundles/codebase/overview/ +--- + +# Codebase bundle overview + +The **Codebase** bundle (`nold-ai/specfact-codebase`) mounts under `specfact code` alongside the Code Review bundle. It focuses on **brownfield import**, **contract coverage analysis**, **drift detection**, **sidecar validation**, and **reproducible validation suites**. + +For automated review runs (Ruff, Semgrep, ledger, rules), see [Code Review](../code-review/overview/) — also on the `code` command group. + +## Prerequisites + +- `specfact module install nold-ai/specfact-codebase` (often together with `nold-ai/specfact-project`) +- Python repos for import/sidecar workflows; optional tool installs (CrossHair, Specmatic, Ruff, etc.) per command help + +## Command groups (`specfact code …`) + +### `import` — brownfield intake + +| Entry | Purpose | +|-------|---------| +| `specfact code import` (default) | Import a repository into a project bundle (`from-code` behavior; see `--help`) | +| `specfact code import from-bridge` | Import from an external bridge/export flow | + +Advanced import topics: [Project import command features](../project/import-migration/) (cross-bundle). + +### `analyze` — structure and contracts + +| Command | Purpose | +|--------|---------| +| `contracts` | Analyze codebase for OpenAPI contract coverage and related signals | + +### `drift` + +| Command | Purpose | +|--------|---------| +| `detect` | Detect drift between implementation and specifications | + +### `validate` — sidecar + +| Command | Purpose | +|--------|---------| +| `sidecar init` | Initialize a sidecar workspace for external-repo validation | +| `sidecar run` | Run the sidecar validation pipeline | + +### `repro` — reproducibility + +| Command | Purpose | +|--------|---------| +| *(default callback)* | Run the full validation suite when no subcommand is given | +| `setup` | Prepare repro validation setup | + +Use `specfact code repro --help` for the default invocation flags (`--repo`, `--verbose`, `--sidecar`, …). + +## Bundle-owned prompts for import/generation + +Import and enrichment flows may ship **prompts or helper templates** with the bundle. They are **bundle payload**, not core-owned assets. Align your IDE with `specfact init ide` after bundle upgrades. + +## Quick examples + +```bash +specfact code import --help +specfact code analyze contracts --help +specfact code drift detect --help +specfact code validate sidecar init my-bundle /path/to/repo +specfact code repro --verbose --repo . +``` + +## Deep dives + +- [Sidecar validation](sidecar-validation/) diff --git a/docs/bundles/govern/overview.md b/docs/bundles/govern/overview.md new file mode 100644 index 0000000..f395c06 --- /dev/null +++ b/docs/bundles/govern/overview.md @@ -0,0 +1,45 @@ +--- +layout: default +title: Govern bundle overview +nav_order: 2 +permalink: /bundles/govern/overview/ +--- + +# Govern bundle overview + +The **Govern** bundle (`nold-ai/specfact-govern`) adds **enforcement** and **patch-mode** workflows: configure quality gates tied to SDD plans, and preview/apply patches with explicit write controls. + +## Prerequisites + +- `specfact module install nold-ai/specfact-govern` +- Project bundles with SDD manifests when using SDD enforcement paths + +## `specfact govern enforce` + +| Command | Purpose | +|--------|---------| +| `stage` | Configure enforcement stages for bundles and plans | +| `sdd` | Enforce SDD quality gates against manifests and plans | + +## `specfact govern patch` + +| Command | Purpose | +|--------|---------| +| `apply` | Preview and apply patches (local or upstream with `--write`) | + +## Bundle-owned policy packs + +Enforcement may ship **bundle-local policy packs or presets** with the package. Treat them as **bundle payload** referenced by the govern module, not as core CLI-owned configuration. Refresh IDE-facing resources with `specfact init ide` when your team updates bundles. + +## Quick examples + +```bash +specfact govern --help +specfact govern enforce stage --help +specfact govern enforce sdd --help +specfact govern patch apply --help +``` + +## See also + +- [Command reference](../../reference/commands/) — nested `govern` commands diff --git a/docs/bundles/project/overview.md b/docs/bundles/project/overview.md new file mode 100644 index 0000000..17113bb --- /dev/null +++ b/docs/bundles/project/overview.md @@ -0,0 +1,94 @@ +--- +layout: default +title: Project bundle overview +nav_order: 2 +permalink: /bundles/project/overview/ +--- + +# Project bundle overview + +The **Project** bundle (`nold-ai/specfact-project`) manages SpecFact **project bundles** (personas, locks, roadmap export), links them to backlog providers, coordinates **development plans** (features, stories, SDD alignment), **syncs** external tools (Spec-Kit, OpenSpec, GitHub, Linear, Jira, …), and **migrates** legacy layouts to bundle-centric structure. + +## Prerequisites + +- SpecFact CLI and a repository with `.specfact/` layout +- Bundle installed: `specfact module install nold-ai/specfact-project` +- For backlog-linked flows: install [Backlog](../backlog/overview/) and link a provider + +## Command families + +The project lifecycle surface loads from this bundle across **`specfact project`**, **`specfact plan`**, top-level **`specfact sync`**, and **`specfact migrate`** (see each group’s `--help` after install). The `project` group also nests a `sync` Typer; prefer the **top-level** `specfact sync …` entry when documented in the command reference. + +### `specfact project` — bundles and personas + +| Command | Purpose | +|--------|---------| +| `link-backlog` | Link a project bundle to a backlog adapter and project id | +| `health-check` | Project health including backlog graph checks | +| `devops-flow` | Integrated DevOps stage actions for a linked bundle | +| `snapshot` | Save the linked backlog graph as a baseline snapshot | +| `regenerate` | Re-derive plan state from bundle + backlog graph | +| `export-roadmap` | Export roadmap milestones from dependency critical path | +| `export` | Export persona-owned sections to Markdown | +| `import` | Import persona-edited Markdown back into the bundle | +| `lock` / `unlock` / `locks` | Section locks for collaborative editing | +| `init-personas` | Initialize persona mappings in the manifest | +| `merge` | Three-way merge with persona-aware conflict handling | +| `resolve-conflict` | Resolve a specific merge conflict | +| `version` | Subcommands for bundle versioning | +| `sync` | Same sync Typer as top-level `specfact sync` (see below) | + +### `specfact plan` — plans, stories, and reviews + +| Command | Purpose | +|--------|---------| +| `init` | Initialize or adopt a development plan for a bundle | +| `add-feature` / `add-story` | Add plan items | +| `update-idea` / `update-feature` / `update-story` | Update plan content | +| `compare` | Compare manual vs automatic plan inputs | +| `select` | Select active plan context | +| `upgrade` | Upgrade plan artifacts | +| `sync` | Sync plan artifacts with repo state | +| `promote` | Promotion workflow for plan readiness | +| `review` | Plan review workflows | +| `harden` | Harden SDD and related artifacts | + +### `specfact sync` — bridges and automation + +Use the top-level group (`specfact sync --help`). + +| Command | Purpose | +|--------|---------| +| `bridge` | Import/export bridge for external tools and adapters | +| `repository` | Repository-scoped sync operations | +| `intelligent` | Higher-level orchestrated sync | + +### `specfact migrate` — structure migrations + +| Command | Purpose | +|--------|---------| +| `cleanup-legacy` | Remove empty legacy top-level directories under `.specfact/` | +| `to-contracts` | Migrate verbose bundles toward contract-oriented layouts | +| `artifacts` | Migrate plan and artifact layouts | + +## Related: codebase import + +Brownfield **code import** (`specfact code import`, `specfact import …`) lives in the [Codebase](../codebase/overview/) bundle; it often feeds project bundles. See [Import command features](import-migration/) for behavior that spans both bundles. + +## Bundle-owned prompts and plan templates + +Plan and review flows may ship **prompts or templates** with the bundle. Treat them as **bundle payload**, not core CLI sources of truth. Refresh IDE-facing resources with `specfact init ide` after upgrades so editors receive the same artifacts the CLI expects. + +## Quick examples + +```bash +specfact project link-backlog --adapter github --project-id owner/repo --bundle my-bundle --repo . +specfact plan init --help +specfact sync bridge --help +specfact migrate artifacts --repo . +``` + +## Deep dives + +- [DevOps flow](devops-flow/) +- [Import command features](import-migration/) diff --git a/docs/bundles/spec/overview.md b/docs/bundles/spec/overview.md new file mode 100644 index 0000000..20e53c6 --- /dev/null +++ b/docs/bundles/spec/overview.md @@ -0,0 +1,75 @@ +--- +layout: default +title: Spec bundle overview +nav_order: 2 +permalink: /bundles/spec/overview/ +--- + +# Spec bundle overview + +The **Spec** bundle (`nold-ai/specfact-spec`) mounts under the **`specfact spec`** command group. That group aggregates **OpenAPI contract lifecycle** commands, **Specmatic** integration (mounted as the `api` subgroup), **SDD** manifest utilities, and **generate** workflows for contracts and prompts. + +Install the bundle, then confirm the mounted tree with `specfact spec --help`. + +## Prerequisites + +- `specfact module install nold-ai/specfact-spec` +- Optional tooling per workflow (Specmatic, OpenAPI files, etc.) + +## `specfact spec contract` — OpenAPI contracts + +| Command | Purpose | +|--------|---------| +| `init` | Initialize contract artifacts for a bundle | +| `validate` | Validate OpenAPI/AsyncAPI specs | +| `coverage` | Report contract coverage signals | +| `serve` | Serve specs for local testing | +| `verify` | Verify contract consistency | +| `test` | Run contract-oriented tests | + +## `specfact spec api` — Specmatic (API spec testing) + +| Command | Purpose | +|--------|---------| +| `validate` | Validate specs via Specmatic | +| `backward-compat` | Compare two spec versions for compatibility | +| `generate-tests` | Generate Specmatic test suites | +| `mock` | Run a Specmatic mock server | + +## `specfact spec sdd` — SDD manifests + +| Command | Purpose | +|--------|---------| +| `list` | List SDD manifests in the repo | +| `constitution bootstrap` | Bootstrap constitution markdown for Spec-Kit compatibility | +| `constitution enrich` | Enrich constitution content | +| `constitution validate` | Validate constitution structure | + +## `specfact spec generate` — generation and prompts + +| Command | Purpose | +|--------|---------| +| `contracts` | Generate contract artifacts from plans | +| `contracts-prompt` | Emit prompts for contract work | +| `contracts-apply` | Apply generated contract changes | +| `fix-prompt` | Prompt-driven fix flows | +| `test-prompt` | Prompt-driven test flows | + +## Bundle-owned prompts + +Generate and contract flows emit **prompts** shipped with the bundle. They are **bundle resources**, not core CLI files. Use `specfact init ide` to refresh IDE exports after bundle upgrades. + +## Quick examples + +```bash +specfact spec --help +specfact spec contract validate --help +specfact spec api validate --help +specfact spec sdd list --repo . +specfact spec generate contracts --help +``` + +## See also + +- [Command reference](../../reference/commands/) — bundle-to-command mapping +- [Contract testing workflow](../../guides/contract-testing-workflow/) diff --git a/docs/index.md b/docs/index.md index 06a0328..976f3d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,14 +13,14 @@ The modules site owns all bundle-specific deep guidance. Core CLI platform docs ## Official Bundles -| Bundle | Description | -|--------|-------------| -| [Backlog](bundles/backlog/refinement/) | AI-assisted refinement, delta commands, dependency analysis, policy engine | -| [Project](bundles/project/devops-flow/) | DevOps flow, import/migration, project setup | -| [Codebase](bundles/codebase/sidecar-validation/) | Sidecar validation, codebase analysis | -| [Spec](reference/commands/) | Specification management | -| [Govern](reference/commands/) | Governance and compliance | -| [Code Review](reference/commands/) | Automated code review with ruff, radon, semgrep | +| Bundle | Overview | Deep dives | +|--------|----------|------------| +| Backlog | [Overview](bundles/backlog/overview/) | [Refinement](bundles/backlog/refinement/), [delta](bundles/backlog/delta/), [policy](bundles/backlog/policy-engine/) | +| Project | [Overview](bundles/project/overview/) | [DevOps flow](bundles/project/devops-flow/), [import features](bundles/project/import-migration/) | +| Codebase | [Overview](bundles/codebase/overview/) | [Sidecar validation](bundles/codebase/sidecar-validation/) | +| Spec | [Overview](bundles/spec/overview/) | [Command reference](reference/commands/) | +| Govern | [Overview](bundles/govern/overview/) | [Command reference](reference/commands/) | +| Code Review | [Overview](bundles/code-review/overview/) | [Module guide](modules/code-review.md) | ## Getting Started diff --git a/openspec/specs/bundle-overview-pages/spec.md b/openspec/specs/bundle-overview-pages/spec.md new file mode 100644 index 0000000..e7beba2 --- /dev/null +++ b/openspec/specs/bundle-overview-pages/spec.md @@ -0,0 +1,37 @@ +# bundle-overview-pages Specification + +## Purpose + +Define requirements for official bundle overview pages on the modules documentation site: each official bundle has a single landing page that lists commands, prerequisites, quick examples, and bundle-owned resource setup guidance aligned with the mounted SpecFact CLI surface. + +## Requirements + +### Requirement: Bundle overview pages SHALL provide complete bundle entry points + +Each official bundle SHALL have a single overview page that lists its commands, prerequisites, examples, and relevant bundle-owned resource setup guidance. + +#### Scenario: Overview page lists all bundle commands + +- **GIVEN** a bundle overview page such as `bundles/backlog/overview.md` +- **WHEN** a user reads the page +- **THEN** every registered command and subcommand for that bundle is listed +- **AND** each command has a brief description + +#### Scenario: Overview page includes quick examples + +- **GIVEN** a bundle overview page +- **WHEN** a user reads the page +- **THEN** at least one practical example is shown for each major command group + +#### Scenario: Overview page explains bundle-owned resource setup when relevant + +- **GIVEN** a bundle overview page for a bundle that ships prompts or workspace templates +- **WHEN** a user reads the page +- **THEN** the page explains which resources are bundled with that package +- **AND** it points to the supported setup flow such as `specfact init ide` or bundle-specific template/bootstrap commands + +#### Scenario: Command examples match actual CLI + +- **GIVEN** a command example in an overview page +- **WHEN** compared against the actual `specfact --help` output +- **THEN** the command name, arguments, and key options match diff --git a/tests/unit/docs/test_docs_review.py b/tests/unit/docs/test_docs_review.py index 71d94f0..1bb2c0f 100644 --- a/tests/unit/docs/test_docs_review.py +++ b/tests/unit/docs/test_docs_review.py @@ -401,6 +401,10 @@ def test_moved_files_have_redirect_from_entries() -> None: rel = path.relative_to(_docs_root()) if not any(str(rel).startswith(d) for d in moved_dirs): continue + # New per-bundle landing pages (not migrated from guides/) have no legacy URL. + parts = rel.parts + if len(parts) >= 3 and parts[0] == "bundles" and path.name == "overview.md": + continue metadata, _ = _split_front_matter(_read_text(path)) if not metadata: continue diff --git a/tests/unit/test_modules_docs_site_contract.py b/tests/unit/test_modules_docs_site_contract.py index 9519d1b..02d709e 100644 --- a/tests/unit/test_modules_docs_site_contract.py +++ b/tests/unit/test_modules_docs_site_contract.py @@ -56,9 +56,12 @@ def test_modules_layout_keeps_sidebar_module_focused() -> None: def test_docs_tree_does_not_reference_retired_public_hosts() -> None: + skip_parts = frozenset({"vendor", "_site", ".bundle", ".jekyll-cache"}) for path in REPO_ROOT.joinpath("docs").rglob("*"): if not path.is_file(): continue + if skip_parts.intersection(path.parts): + continue text = _read(path) for host in OUTDATED_DOCS_HOSTS: assert host not in text, f"{path} still references retired host {host}" From b93e2c7bc73910a8603eea64cd073938bdcc12d9 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 25 Mar 2026 23:39:40 +0100 Subject: [PATCH 2/8] docs: address bundle overview and index review feedback - Backlog: document auth subcommands (azure-devops, github, status, clear) - Codebase: user-facing repro default row; code-review module link permalink - Govern: init ide plus code review rules init/update --ide with SupportedIde - Project: rename Deep dives to See also - Spec: nest constitution subcommands; add constitution --help example - Index: Code Review deep dive link uses modules/code-review/ Made-with: Cursor --- docs/bundles/backlog/overview.md | 16 ++++++++++++---- docs/bundles/code-review/overview.md | 2 +- docs/bundles/codebase/overview.md | 2 +- docs/bundles/govern/overview.md | 2 +- docs/bundles/project/overview.md | 2 +- docs/bundles/spec/overview.md | 12 +++++++++--- docs/index.md | 2 +- 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/bundles/backlog/overview.md b/docs/bundles/backlog/overview.md index ff99f4f..56c1c44 100644 --- a/docs/bundles/backlog/overview.md +++ b/docs/bundles/backlog/overview.md @@ -65,11 +65,16 @@ Deterministic policy validation against backlog snapshots (bundled with this pac See [Policy engine](policy-engine.md) for details. -### Other +### Auth (`specfact backlog auth`) -| Command | Purpose | -|--------|---------| -| `auth` | Authenticate backlog providers | +Backlog provider authentication for **GitHub** and **Azure DevOps**: store tokens, run OAuth / device-code flows, inspect status, and clear credentials for setup and troubleshooting. + +| Subcommand | Purpose | +|------------|---------| +| `azure-devops` | Authenticate to Azure DevOps via PAT (`--pat`) or OAuth (interactive browser or `--use-device-code`) | +| `github` | Authenticate to GitHub (or GitHub Enterprise) via RFC 8628 device code flow; optional `--client-id`, `--base-url`, `--scopes` | +| `status` | Show stored auth state for supported providers (valid vs expired tokens) | +| `clear` | Remove stored tokens; optional `--provider` (`azure-devops` or `github`) or omit to clear all | ## Bundle-owned prompts and templates @@ -84,6 +89,9 @@ specfact backlog daily --help specfact backlog refine --help specfact backlog delta status --help specfact backlog policy validate --help +specfact backlog auth --help +specfact backlog auth status --help +specfact backlog auth github --help ``` GitHub refinement preview (typical entry point): diff --git a/docs/bundles/code-review/overview.md b/docs/bundles/code-review/overview.md index 8aa8cb6..0d2b2fa 100644 --- a/docs/bundles/code-review/overview.md +++ b/docs/bundles/code-review/overview.md @@ -54,5 +54,5 @@ specfact code review rules show --help ## See also -- [Code review module](../../modules/code-review.md) +- [Code review module](../../modules/code-review/) - [Codebase bundle overview](../codebase/overview/) — import, drift, validation, repro diff --git a/docs/bundles/codebase/overview.md b/docs/bundles/codebase/overview.md index 3663eb9..9658b7d 100644 --- a/docs/bundles/codebase/overview.md +++ b/docs/bundles/codebase/overview.md @@ -50,7 +50,7 @@ Advanced import topics: [Project import command features](../project/import-migr | Command | Purpose | |--------|---------| -| *(default callback)* | Run the full validation suite when no subcommand is given | +| Default when no subcommand | Run the full validation suite | | `setup` | Prepare repro validation setup | Use `specfact code repro --help` for the default invocation flags (`--repo`, `--verbose`, `--sidecar`, …). diff --git a/docs/bundles/govern/overview.md b/docs/bundles/govern/overview.md index f395c06..0eb3df2 100644 --- a/docs/bundles/govern/overview.md +++ b/docs/bundles/govern/overview.md @@ -29,7 +29,7 @@ The **Govern** bundle (`nold-ai/specfact-govern`) adds **enforcement** and **pat ## Bundle-owned policy packs -Enforcement may ship **bundle-local policy packs or presets** with the package. Treat them as **bundle payload** referenced by the govern module, not as core CLI-owned configuration. Refresh IDE-facing resources with `specfact init ide` when your team updates bundles. +Enforcement may ship **bundle-local policy packs or presets** with the package. Treat them as **bundle payload** referenced by the govern module, not as core CLI-owned configuration. Refresh IDE-facing resources with `specfact init ide` (core IDE prompts from installed modules). When updating the Code Review **house-rules** skill, use `specfact code review rules init --ide` or `specfact code review rules update --ide` (e.g. `--ide cursor`, `--ide claude`, `--ide codex`, or `--ide vibe`; see `--help`). ## Quick examples diff --git a/docs/bundles/project/overview.md b/docs/bundles/project/overview.md index 17113bb..d02b226 100644 --- a/docs/bundles/project/overview.md +++ b/docs/bundles/project/overview.md @@ -88,7 +88,7 @@ specfact sync bridge --help specfact migrate artifacts --repo . ``` -## Deep dives +## See also - [DevOps flow](devops-flow/) - [Import command features](import-migration/) diff --git a/docs/bundles/spec/overview.md b/docs/bundles/spec/overview.md index 20e53c6..54f1b82 100644 --- a/docs/bundles/spec/overview.md +++ b/docs/bundles/spec/overview.md @@ -41,9 +41,14 @@ Install the bundle, then confirm the mounted tree with `specfact spec --help`. | Command | Purpose | |--------|---------| | `list` | List SDD manifests in the repo | -| `constitution bootstrap` | Bootstrap constitution markdown for Spec-Kit compatibility | -| `constitution enrich` | Enrich constitution content | -| `constitution validate` | Validate constitution structure | + +### `constitution` subcommands + +| Subcommand | Purpose | +|------------|---------| +| `bootstrap` | Bootstrap constitution markdown for Spec-Kit compatibility | +| `enrich` | Enrich constitution content | +| `validate` | Validate constitution structure | ## `specfact spec generate` — generation and prompts @@ -66,6 +71,7 @@ specfact spec --help specfact spec contract validate --help specfact spec api validate --help specfact spec sdd list --repo . +specfact spec sdd constitution validate --help specfact spec generate contracts --help ``` diff --git a/docs/index.md b/docs/index.md index 976f3d1..4fae584 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ The modules site owns all bundle-specific deep guidance. Core CLI platform docs | Codebase | [Overview](bundles/codebase/overview/) | [Sidecar validation](bundles/codebase/sidecar-validation/) | | Spec | [Overview](bundles/spec/overview/) | [Command reference](reference/commands/) | | Govern | [Overview](bundles/govern/overview/) | [Command reference](reference/commands/) | -| Code Review | [Overview](bundles/code-review/overview/) | [Module guide](modules/code-review.md) | +| Code Review | [Overview](bundles/code-review/overview/) | [Module guide](modules/code-review/) | ## Getting Started From 4d331bad69151f0194b3f811964d38996a0bbb97 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 25 Mar 2026 23:45:59 +0100 Subject: [PATCH 3/8] docs(backlog): use directory permalink for Policy engine link Made-with: Cursor --- docs/bundles/backlog/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bundles/backlog/overview.md b/docs/bundles/backlog/overview.md index 56c1c44..7b252c9 100644 --- a/docs/bundles/backlog/overview.md +++ b/docs/bundles/backlog/overview.md @@ -63,7 +63,7 @@ Deterministic policy validation against backlog snapshots (bundled with this pac | `validate` | Run policy checks (JSON/Markdown/both) | | `suggest` | Patch-ready suggestions (no automatic writes) | -See [Policy engine](policy-engine.md) for details. +See [Policy engine](policy-engine/) for details. ### Auth (`specfact backlog auth`) From 53c0877eccdb6ba4d8dd3783cdce30e29946b1c8 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Thu, 26 Mar 2026 00:22:30 +0100 Subject: [PATCH 4/8] fix review findings --- .gitignore | 1 + docs/_config.yml | 4 + docs/bundles/codebase/overview.md | 2 +- docs/bundles/govern/overview.md | 2 +- docs/modules/code-review.md | 7 + openspec/specs/bundle-overview-pages/spec.md | 1 + .../docs/test_bundle_overview_cli_examples.py | 189 ++++++++++++++++++ tests/unit/docs/test_docs_review.py | 5 +- 8 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 tests/unit/docs/test_bundle_overview_cli_examples.py diff --git a/.gitignore b/.gitignore index 6e50d0b..c117fa2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ logs/ # Jekyll docs — local Bundler state and build output docs/.bundle/ docs/_site/ +docs/.jekyll-cache/ # local bundle installs (bundle config path vendor/bundle) docs/vendor/ diff --git a/docs/_config.yml b/docs/_config.yml index bb74f62..7096267 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -81,6 +81,10 @@ defaults: path: "authoring" values: layout: default + - scope: + path: "modules" + values: + layout: default theme: minima minima: social: diff --git a/docs/bundles/codebase/overview.md b/docs/bundles/codebase/overview.md index 9658b7d..dc49d37 100644 --- a/docs/bundles/codebase/overview.md +++ b/docs/bundles/codebase/overview.md @@ -50,7 +50,7 @@ Advanced import topics: [Project import command features](../project/import-migr | Command | Purpose | |--------|---------| -| Default when no subcommand | Run the full validation suite | +| `repro` (default) | Run the full validation suite | | `setup` | Prepare repro validation setup | Use `specfact code repro --help` for the default invocation flags (`--repo`, `--verbose`, `--sidecar`, …). diff --git a/docs/bundles/govern/overview.md b/docs/bundles/govern/overview.md index 0eb3df2..7c1edd6 100644 --- a/docs/bundles/govern/overview.md +++ b/docs/bundles/govern/overview.md @@ -29,7 +29,7 @@ The **Govern** bundle (`nold-ai/specfact-govern`) adds **enforcement** and **pat ## Bundle-owned policy packs -Enforcement may ship **bundle-local policy packs or presets** with the package. Treat them as **bundle payload** referenced by the govern module, not as core CLI-owned configuration. Refresh IDE-facing resources with `specfact init ide` (core IDE prompts from installed modules). When updating the Code Review **house-rules** skill, use `specfact code review rules init --ide` or `specfact code review rules update --ide` (e.g. `--ide cursor`, `--ide claude`, `--ide codex`, or `--ide vibe`; see `--help`). +Enforcement may ship **bundle-local policy packs or presets** with the package. Treat them as **bundle payload** referenced by the govern module, not as core CLI-owned configuration. Refresh IDE-facing resources with `specfact init ide` (core IDE prompts from installed modules). The **Code Review** bundle (`nold-ai/specfact-code-review`) provides `specfact code review rules init --ide` and `specfact code review rules update --ide` (e.g. `--ide cursor`, `--ide claude`, `--ide codex`, or `--ide vibe`; see `--help`) for the house-rules skill—install that bundle separately when you need those commands. ## Quick examples diff --git a/docs/modules/code-review.md b/docs/modules/code-review.md index b91715a..77038e7 100644 --- a/docs/modules/code-review.md +++ b/docs/modules/code-review.md @@ -1,3 +1,10 @@ +--- +layout: default +title: Code review module +nav_order: 10 +permalink: /modules/code-review/ +--- + # specfact-code-review module notes ## Review run command diff --git a/openspec/specs/bundle-overview-pages/spec.md b/openspec/specs/bundle-overview-pages/spec.md index e7beba2..5950e80 100644 --- a/openspec/specs/bundle-overview-pages/spec.md +++ b/openspec/specs/bundle-overview-pages/spec.md @@ -35,3 +35,4 @@ Each official bundle SHALL have a single overview page that lists its commands, - **GIVEN** a command example in an overview page - **WHEN** compared against the actual `specfact --help` output - **THEN** the command name, arguments, and key options match +- **AND** `tests/unit/docs/test_bundle_overview_cli_examples.py::test_validate_bundle_overview_cli_help_examples` exercises each quick-example line by invoking the corresponding bundle Typer app with `--help` (or an explicit `--help` normalization for lines that include runnable flags), failing when help output cannot be produced diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py new file mode 100644 index 0000000..17ec103 --- /dev/null +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -0,0 +1,189 @@ +"""Validate bundle overview quick examples against bundle Typer apps (`--help`). + +Implements the bundle-overview-pages spec scenario: command examples stay aligned with +`specfact --help`. The root `specfact` CLI may omit nested members in some dev +setups, so we invoke the same Typer apps the CLI mounts from each official bundle. +""" + +from __future__ import annotations + +import importlib +import re +import shlex +from pathlib import Path +from typing import Any + +from typer.testing import CliRunner + + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_BUNDLE_OVERVIEWS = sorted(_REPO_ROOT.glob("docs/bundles/*/overview.md")) + +# Runnable examples (not plain `--help`); map exact line → tokens after `specfact`. +_OVERVIEW_LINE_TO_TOKENS_AFTER_SPECFACT: dict[str, list[str]] = { + "specfact code validate sidecar init my-bundle /path/to/repo": [ + "code", + "validate", + "sidecar", + "init", + "--help", + ], + "specfact code repro --verbose --repo .": ["code", "repro", "--help"], + "specfact project link-backlog --adapter github --project-id owner/repo --bundle my-bundle --repo .": [ + "project", + "link-backlog", + "--help", + ], + "specfact migrate artifacts --repo .": ["migrate", "artifacts", "--help"], + "specfact backlog refine github --preview --labels feature": [ + "backlog", + "refine", + "github", + "--help", + ], + "specfact backlog daily github --state open --limit 20": ["backlog", "daily", "--help"], + "specfact spec sdd list --repo .": ["spec", "sdd", "list", "--repo", ".", "--help"], +} + +_BASH_FENCE_RE = re.compile(r"^```(?:bash)?\s*$") + + +def _iter_bash_block_lines(markdown: str) -> list[str]: + lines_out: list[str] = [] + in_bash = False + for raw in markdown.splitlines(): + if _BASH_FENCE_RE.match(raw.strip()): + in_bash = not in_bash + continue + if in_bash: + lines_out.append(raw.rstrip("\n")) + return lines_out + + +def _tokens_for_specfact_line(line: str) -> list[str] | None: + s = line.strip() + if not s or s.startswith("#"): + return None + if "&&" in s or "|" in s: + return None + if s in _OVERVIEW_LINE_TO_TOKENS_AFTER_SPECFACT: + return ["specfact", *_OVERVIEW_LINE_TO_TOKENS_AFTER_SPECFACT[s]] + try: + tokens = shlex.split(s) + except ValueError: + return None + if not tokens or tokens[0] != "specfact": + return None + return tokens + + +def _route_to_bundle_app_and_argv(tokens: list[str]) -> tuple[Any, list[str]] | None: + """Map `specfact …` tokens to (Typer app, argv for CliRunner).""" + if len(tokens) < 2 or tokens[0] != "specfact": + return None + t = tokens[1:] + + if t[0] == "backlog" and len(t) > 1 and t[1] == "policy": + mod = importlib.import_module("specfact_backlog.policy_engine.commands") + return mod.app, t[2:] + + if t[0] == "backlog": + mod = importlib.import_module("specfact_backlog.backlog.commands") + return mod.app, t[1:] + + if t[0] == "govern": + mod = importlib.import_module("specfact_govern.govern.commands") + return mod.app, t[1:] + + if t[0] == "project": + mod = importlib.import_module("specfact_project.project.commands") + return mod.app, t[1:] + + if t[0] == "plan": + mod = importlib.import_module("specfact_project.plan.commands") + return mod.app, t[1:] + + if t[0] == "sync": + mod = importlib.import_module("specfact_project.sync.commands") + return mod.app, t[1:] + + if t[0] == "migrate": + mod = importlib.import_module("specfact_project.migrate.commands") + return mod.app, t[1:] + + if t[0] == "code" and len(t) > 1: + if t[1] == "review": + mod = importlib.import_module("specfact_code_review.review.commands") + # `review.commands.app` mounts the nested `review` subgroup at name `review`. + return mod.app, t[1:] + if t[1] == "import": + mod = importlib.import_module("specfact_codebase.import_cmd.commands") + return mod.app, t[2:] + if t[1] == "analyze": + mod = importlib.import_module("specfact_codebase.analyze.commands") + return mod.app, t[2:] + if t[1] == "drift": + mod = importlib.import_module("specfact_codebase.drift.commands") + return mod.app, t[2:] + if t[1] == "validate": + mod = importlib.import_module("specfact_codebase.validate.commands") + return mod.app, t[2:] + if t[1] == "repro": + mod = importlib.import_module("specfact_codebase.repro.commands") + return mod.app, t[2:] + + if t[0] == "spec" and len(t) == 2 and t[1] == "--help": + mod = importlib.import_module("specfact_cli.groups.spec_group") + return mod.build_app(), ["--help"] + + if t[0] == "spec" and len(t) > 1: + sub = t[1] + if sub == "contract": + mod = importlib.import_module("specfact_spec.contract.commands") + elif sub == "api": + mod = importlib.import_module("specfact_spec.spec.commands") + elif sub == "sdd": + mod = importlib.import_module("specfact_spec.sdd.commands") + elif sub == "generate": + mod = importlib.import_module("specfact_spec.generate.commands") + else: + return None + return mod.app, t[2:] + + return None + + +def test_validate_bundle_overview_cli_help_examples() -> None: + """Invoke bundle Typer apps with argv derived from each overview quick-example line.""" + runner = CliRunner() + seen: set[str] = set() + failures: list[str] = [] + + for overview in _BUNDLE_OVERVIEWS: + text = overview.read_text(encoding="utf-8") + for raw_line in _iter_bash_block_lines(text): + tokens = _tokens_for_specfact_line(raw_line) + if tokens is None: + continue + if "--help" not in tokens: + failures.append( + f"{overview.relative_to(_REPO_ROOT)}: {raw_line.strip()!r} " + "(add --help to the example or an entry in _OVERVIEW_LINE_TO_TOKENS_AFTER_SPECFACT)" + ) + continue + + key = " ".join(tokens) + if key in seen: + continue + seen.add(key) + + routed = _route_to_bundle_app_and_argv(tokens) + if routed is None: + failures.append(f"{overview.relative_to(_REPO_ROOT)}: no route for {key!r}") + continue + app, argv = routed + result = runner.invoke(app, argv, prog_name="specfact") + if result.exit_code != 0: + failures.append(f"{overview.relative_to(_REPO_ROOT)}: {raw_line.strip()!r} -> exit {result.exit_code}") + + assert not failures, "Overview CLI --help mismatches:\n" + "\n".join(failures) diff --git a/tests/unit/docs/test_docs_review.py b/tests/unit/docs/test_docs_review.py index 1bb2c0f..d0977bd 100644 --- a/tests/unit/docs/test_docs_review.py +++ b/tests/unit/docs/test_docs_review.py @@ -33,8 +33,11 @@ def _docs_root() -> Path: return _repo_root() / "docs" +_SKIP_DOCS_TREE_PARTS = frozenset({"_site", "vendor", ".bundle", ".jekyll-cache"}) + + def _is_docs_markdown(path: Path) -> bool: - return path.suffix == ".md" and "_site" not in path.parts and "vendor" not in path.parts + return path.suffix == ".md" and not _SKIP_DOCS_TREE_PARTS.intersection(path.parts) def _is_publishable_page(path: Path) -> bool: From 39cdc086086ee12afc1d71308e20c020e885ae4e Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Thu, 26 Mar 2026 00:28:25 +0100 Subject: [PATCH 5/8] fix test issue --- .../docs/test_bundle_overview_cli_examples.py | 143 ++++++++++-------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index 17ec103..8f9e0c9 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -77,79 +77,92 @@ def _tokens_for_specfact_line(line: str) -> list[str] | None: return tokens -def _route_to_bundle_app_and_argv(tokens: list[str]) -> tuple[Any, list[str]] | None: - """Map `specfact …` tokens to (Typer app, argv for CliRunner).""" - if len(tokens) < 2 or tokens[0] != "specfact": +def _route_backlog(t: list[str]) -> tuple[Any, list[str]] | None: + if not t or t[0] != "backlog": return None - t = tokens[1:] - - if t[0] == "backlog" and len(t) > 1 and t[1] == "policy": + if len(t) > 1 and t[1] == "policy": mod = importlib.import_module("specfact_backlog.policy_engine.commands") return mod.app, t[2:] + mod = importlib.import_module("specfact_backlog.backlog.commands") + return mod.app, t[1:] + + +_TOP_LEVEL_MODULE_BY_PREFIX: dict[str, str] = { + "govern": "specfact_govern.govern.commands", + "project": "specfact_project.project.commands", + "plan": "specfact_project.plan.commands", + "sync": "specfact_project.sync.commands", + "migrate": "specfact_project.migrate.commands", +} - if t[0] == "backlog": - mod = importlib.import_module("specfact_backlog.backlog.commands") - return mod.app, t[1:] - - if t[0] == "govern": - mod = importlib.import_module("specfact_govern.govern.commands") - return mod.app, t[1:] - - if t[0] == "project": - mod = importlib.import_module("specfact_project.project.commands") - return mod.app, t[1:] - - if t[0] == "plan": - mod = importlib.import_module("specfact_project.plan.commands") - return mod.app, t[1:] - - if t[0] == "sync": - mod = importlib.import_module("specfact_project.sync.commands") - return mod.app, t[1:] - - if t[0] == "migrate": - mod = importlib.import_module("specfact_project.migrate.commands") - return mod.app, t[1:] - - if t[0] == "code" and len(t) > 1: - if t[1] == "review": - mod = importlib.import_module("specfact_code_review.review.commands") - # `review.commands.app` mounts the nested `review` subgroup at name `review`. - return mod.app, t[1:] - if t[1] == "import": - mod = importlib.import_module("specfact_codebase.import_cmd.commands") - return mod.app, t[2:] - if t[1] == "analyze": - mod = importlib.import_module("specfact_codebase.analyze.commands") - return mod.app, t[2:] - if t[1] == "drift": - mod = importlib.import_module("specfact_codebase.drift.commands") - return mod.app, t[2:] - if t[1] == "validate": - mod = importlib.import_module("specfact_codebase.validate.commands") - return mod.app, t[2:] - if t[1] == "repro": - mod = importlib.import_module("specfact_codebase.repro.commands") - return mod.app, t[2:] - - if t[0] == "spec" and len(t) == 2 and t[1] == "--help": + +def _route_top_level(t: list[str]) -> tuple[Any, list[str]] | None: + if not t: + return None + mod_path = _TOP_LEVEL_MODULE_BY_PREFIX.get(t[0]) + if mod_path is None: + return None + mod = importlib.import_module(mod_path) + return mod.app, t[1:] + + +# Subcommand under `specfact code …` → Typer module. `review` keeps `review` in argv (nested subgroup). +_CODE_SUB_TO_MODULE: dict[str, tuple[str, bool]] = { + "review": ("specfact_code_review.review.commands", True), + "import": ("specfact_codebase.import_cmd.commands", False), + "analyze": ("specfact_codebase.analyze.commands", False), + "drift": ("specfact_codebase.drift.commands", False), + "validate": ("specfact_codebase.validate.commands", False), + "repro": ("specfact_codebase.repro.commands", False), +} + + +def _route_code(t: list[str]) -> tuple[Any, list[str]] | None: + if len(t) < 2 or t[0] != "code": + return None + entry = _CODE_SUB_TO_MODULE.get(t[1]) + if entry is None: + return None + mod_path, keep_review_prefix = entry + mod = importlib.import_module(mod_path) + argv = t[1:] if keep_review_prefix else t[2:] + return mod.app, argv + + +_SPEC_SUB_TO_MODULE = { + "contract": "specfact_spec.contract.commands", + "api": "specfact_spec.spec.commands", + "sdd": "specfact_spec.sdd.commands", + "generate": "specfact_spec.generate.commands", +} + + +def _route_spec(t: list[str]) -> tuple[Any, list[str]] | None: + if not t or t[0] != "spec": + return None + if len(t) == 2 and t[1] == "--help": mod = importlib.import_module("specfact_cli.groups.spec_group") return mod.build_app(), ["--help"] + if len(t) < 2: + return None + mod_path = _SPEC_SUB_TO_MODULE.get(t[1]) + if mod_path is None: + return None + mod = importlib.import_module(mod_path) + return mod.app, t[2:] - if t[0] == "spec" and len(t) > 1: - sub = t[1] - if sub == "contract": - mod = importlib.import_module("specfact_spec.contract.commands") - elif sub == "api": - mod = importlib.import_module("specfact_spec.spec.commands") - elif sub == "sdd": - mod = importlib.import_module("specfact_spec.sdd.commands") - elif sub == "generate": - mod = importlib.import_module("specfact_spec.generate.commands") - else: - return None - return mod.app, t[2:] +def _route_to_bundle_app_and_argv(tokens: list[str]) -> tuple[Any, list[str]] | None: + """Map `specfact …` tokens to (Typer app, argv for CliRunner).""" + if len(tokens) < 2 or tokens[0] != "specfact": + return None + t = tokens[1:] + if not t: + return None + for router in (_route_backlog, _route_top_level, _route_code, _route_spec): + routed = router(t) + if routed is not None: + return routed return None From e2101e3a4126cc0e7c70fad36bce7aeac48367ec Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Thu, 26 Mar 2026 00:33:54 +0100 Subject: [PATCH 6/8] Fix test --- .../docs/test_bundle_overview_cli_examples.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index 8f9e0c9..0dda702 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -8,6 +8,7 @@ from __future__ import annotations import importlib +import logging import re import shlex from pathlib import Path @@ -42,7 +43,7 @@ "--help", ], "specfact backlog daily github --state open --limit 20": ["backlog", "daily", "--help"], - "specfact spec sdd list --repo .": ["spec", "sdd", "list", "--repo", ".", "--help"], + "specfact spec sdd list --repo .": ["spec", "sdd", "list", "--help"], } _BASH_FENCE_RE = re.compile(r"^```(?:bash)?\s*$") @@ -145,9 +146,12 @@ def _route_spec(t: list[str]) -> tuple[Any, list[str]] | None: return mod.build_app(), ["--help"] if len(t) < 2: return None - mod_path = _SPEC_SUB_TO_MODULE.get(t[1]) + sub = t[1] + mod_path = _SPEC_SUB_TO_MODULE.get(sub) if mod_path is None: - return None + logging.getLogger(__name__).warning("Unrecognized spec subcommand: %s - tokens: %s", sub, t) + msg = f"Unrecognized spec subcommand: {sub!r} (tokens: {t!r})" + raise ValueError(msg) mod = importlib.import_module(mod_path) return mod.app, t[2:] @@ -195,8 +199,19 @@ def test_validate_bundle_overview_cli_help_examples() -> None: failures.append(f"{overview.relative_to(_REPO_ROOT)}: no route for {key!r}") continue app, argv = routed + # We only assert Typer accepted argv and exited 0; we do not diff full --help text or + # every option name against the markdown (that would be brittle and duplicate Typer). + # Optional smoke check below ensures something help-like was printed when --help is used. result = runner.invoke(app, argv, prog_name="specfact") if result.exit_code != 0: failures.append(f"{overview.relative_to(_REPO_ROOT)}: {raw_line.strip()!r} -> exit {result.exit_code}") + continue + if "--help" in argv: + # CliRunner may expose only `output` (stdout+stderr) unless mix_stderr=False. + combined = result.output or "" + if "Usage" not in combined and "usage:" not in combined.lower(): + failures.append( + f"{overview.relative_to(_REPO_ROOT)}: {raw_line.strip()!r} -> help output missing Usage banner" + ) assert not failures, "Overview CLI --help mismatches:\n" + "\n".join(failures) From 0a3f48629a65fb3546753b744accfb45de68cff1 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Thu, 26 Mar 2026 00:35:52 +0100 Subject: [PATCH 7/8] Apply review fixes --- .../docs/test_bundle_overview_cli_examples.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index 0dda702..f4008df 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -46,15 +46,20 @@ "specfact spec sdd list --repo .": ["spec", "sdd", "list", "--help"], } -_BASH_FENCE_RE = re.compile(r"^```(?:bash)?\s*$") +_OPEN_BASH_FENCE_RE = re.compile(r"^```bash\s*$") +_CLOSE_FENCE_RE = re.compile(r"^```\s*$") def _iter_bash_block_lines(markdown: str) -> list[str]: lines_out: list[str] = [] in_bash = False for raw in markdown.splitlines(): - if _BASH_FENCE_RE.match(raw.strip()): - in_bash = not in_bash + stripped = raw.strip() + if _OPEN_BASH_FENCE_RE.match(stripped): + in_bash = True + continue + if _CLOSE_FENCE_RE.match(stripped): + in_bash = False continue if in_bash: lines_out.append(raw.rstrip("\n")) @@ -173,9 +178,10 @@ def _route_to_bundle_app_and_argv(tokens: list[str]) -> tuple[Any, list[str]] | def test_validate_bundle_overview_cli_help_examples() -> None: """Invoke bundle Typer apps with argv derived from each overview quick-example line.""" runner = CliRunner() - seen: set[str] = set() + seen: set[tuple[str, ...]] = set() failures: list[str] = [] + assert _BUNDLE_OVERVIEWS, "no bundle overviews discovered" for overview in _BUNDLE_OVERVIEWS: text = overview.read_text(encoding="utf-8") for raw_line in _iter_bash_block_lines(text): @@ -189,14 +195,15 @@ def test_validate_bundle_overview_cli_help_examples() -> None: ) continue - key = " ".join(tokens) - if key in seen: + dedupe_key = tuple(tokens) + display_key = " ".join(tokens) + if dedupe_key in seen: continue - seen.add(key) + seen.add(dedupe_key) routed = _route_to_bundle_app_and_argv(tokens) if routed is None: - failures.append(f"{overview.relative_to(_REPO_ROOT)}: no route for {key!r}") + failures.append(f"{overview.relative_to(_REPO_ROOT)}: no route for {display_key!r}") continue app, argv = routed # We only assert Typer accepted argv and exited 0; we do not diff full --help text or From 44c118b0ff5d8b4ae359e2a56df634db4a305a82 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Thu, 26 Mar 2026 00:43:51 +0100 Subject: [PATCH 8/8] Fix review findings --- .../docs/test_bundle_overview_cli_examples.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index f4008df..0fb7b4a 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import Any +from typer.main import get_command as typer_get_command from typer.testing import CliRunner @@ -143,12 +144,28 @@ def _route_code(t: list[str]) -> tuple[Any, list[str]] | None: } +def _spec_group_help_app_or_contract_fallback() -> tuple[Any, list[str]]: + """`specfact spec --help` uses the spec category group when CommandRegistry mounts members. + + In CI the group Typer can be empty (no bundle modules registered yet); Typer then raises + when building a Click command. Fall back to a representative bundle sub-app so the test + still validates a real `--help` surface. + """ + spec_group_mod = importlib.import_module("specfact_cli.groups.spec_group") + app = spec_group_mod.build_app() + try: + typer_get_command(app) + except RuntimeError: + contract_mod = importlib.import_module("specfact_spec.contract.commands") + return contract_mod.app, ["--help"] + return app, ["--help"] + + def _route_spec(t: list[str]) -> tuple[Any, list[str]] | None: if not t or t[0] != "spec": return None if len(t) == 2 and t[1] == "--help": - mod = importlib.import_module("specfact_cli.groups.spec_group") - return mod.build_app(), ["--help"] + return _spec_group_help_app_or_contract_fallback() if len(t) < 2: return None sub = t[1]