diff --git a/.github/workflows/docs-review.yml b/.github/workflows/docs-review.yml index a47445af..00dd519d 100644 --- a/.github/workflows/docs-review.yml +++ b/.github/workflows/docs-review.yml @@ -21,6 +21,7 @@ on: - "tests/helpers/doc_frontmatter_types.py" - "scripts/check-docs-commands.py" - "scripts/check-command-contract.py" + - "scripts/check-documentation-accountability.py" - "scripts/generate-command-overview.py" - "docs/reference/commands.generated.*" - "llms.txt" @@ -49,6 +50,7 @@ on: - "tests/helpers/doc_frontmatter_types.py" - "scripts/check-docs-commands.py" - "scripts/check-command-contract.py" + - "scripts/check-documentation-accountability.py" - "scripts/generate-command-overview.py" - "docs/reference/commands.generated.*" - "llms.txt" @@ -121,6 +123,9 @@ jobs: - name: Validate generated command contract run: hatch run check-command-contract + - name: Validate documentation accountability + run: hatch run check-documentation-accountability + - name: Cross-site links (warn-only; live site may lag deploys) continue-on-error: true run: hatch run check-cross-site-links --warn-only diff --git a/CHANGELOG.md b/CHANGELOG.md index f720dfaf..79acd34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ All notable changes to this project will be documented in this file. --- +## [0.51.2] - 2026-07-10 + +### Fixed + +- **Documentation accountability**: derive the official module inventory from + signed module manifests and the marketplace registry, then fail local + pre-commit and PR documentation validation when core catalogues, generated + command records, or core/modules ownership statements drift. The repaired + catalogues now cover the installed requirements and code-review modules. + +--- + ## [0.51.1] - 2026-07-10 ### Fixed diff --git a/README.md b/README.md index de9f95ea..0a622f94 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,11 @@ For cleanup work, run a JSON review, inspect the cleanup forecast and AI-bloat i These findings are bloat-shape detection and cleanup guidance, not AI-authorship detection. Exact simplify flags and report fields live in the [AI bloat quickstart](https://modules.specfact.io/quickstart-ai-bloat/) and [Code Review run guide](https://modules.specfact.io/bundles/code-review/run/) on the modules docs site. +Official workflow package IDs are `nold-ai/specfact-project`, +`nold-ai/specfact-backlog`, `nold-ai/specfact-codebase`, +`nold-ai/specfact-code-review`, `nold-ai/specfact-spec`, +`nold-ai/specfact-govern`, and `nold-ai/specfact-requirements`. + ## What SpecFact does - **Defends against AI bloat deterministically** — forecast cleanup impact and route remediation packets to your AI IDE @@ -163,6 +168,7 @@ specfact module install nold-ai/specfact-backlog specfact module install nold-ai/specfact-codebase specfact module install nold-ai/specfact-code-review specfact module install nold-ai/specfact-govern +specfact module install nold-ai/specfact-spec specfact module install nold-ai/specfact-requirements ``` diff --git a/docs/.doc-frontmatter-enforced b/docs/.doc-frontmatter-enforced index 956e6474..413217dd 100644 --- a/docs/.doc-frontmatter-enforced +++ b/docs/.doc-frontmatter-enforced @@ -6,4 +6,11 @@ docs/core-cli/init.md docs/reference/documentation-url-contract.md docs/contributing/docs-sync.md docs/contributing/frontmatter-schema.md +docs/module-system/marketplace.md +docs/reference/commands.md +docs/reference/module-categories.md +docs/reference/directory-structure.md +docs/getting-started/installation.md +docs/architecture/overview.md +docs/architecture/implementation-status.md docs/agent-rules/*.md diff --git a/docs/architecture/implementation-status.md b/docs/architecture/implementation-status.md index 81ef170d..949a8d44 100644 --- a/docs/architecture/implementation-status.md +++ b/docs/architecture/implementation-status.md @@ -6,6 +6,11 @@ description: Implemented vs planned architecture capabilities. keywords: [implementation status, roadmap, architecture] audience: [team, enterprise] expertise_level: [advanced] +doc_owner: specfact-cli +tracks: [src/specfact_cli/**, openspec/**, ../specfact-cli-modules/packages/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Architecture Implementation Status @@ -23,7 +28,8 @@ This page tracks implemented vs planned architecture capabilities. - Shared architecture and traceability contracts remain planned in OpenSpec. - Source: `architecture-01-solution-layer`, `traceability-01-index-and-orphans`, and related follow-up changes. -- Older flat command families such as `specfact architecture ...`, `specfact requirements ...`, and `specfact trace ...` are not canonical delivery targets. +- Older flat core command families such as `specfact architecture ...` and `specfact trace ...` are not canonical delivery targets. `specfact requirements ...` is instead an installed grouped surface owned by `nold-ai/specfact-requirements`. +- Canonical bundle-deep documentation is owned by the [modules documentation site](https://modules.specfact.io/). - Any future runtime implementation must be rescoped into grouped bundle-owned surfaces in `specfact-cli-modules`; core retains only shared models, schemas, and integration contracts. - Protocol FSM runtime engine is partial (models/specs exist; full execution engine/guards are planned). - Source: OpenSpec `architecture-01-solution-layer` and dependent validation changes. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index fd5a718c..cbc1f719 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -7,6 +7,11 @@ redirect_from: keywords: [architecture, module system, runtime, registry] audience: [team, enterprise] expertise_level: [intermediate, advanced] +doc_owner: specfact-cli +tracks: [src/specfact_cli/registry/**, src/specfact_cli/modules/**, ../specfact-cli-modules/packages/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Architecture @@ -16,8 +21,9 @@ SpecFact CLI is a contract-first Python CLI with a production-ready module regis ## Current Architecture Status - Module system is **production-ready** (introduced in `v0.27`) and is the default command-loading path. -- Older flat command concepts such as `specfact architecture ...`, `specfact requirements ...`, and `specfact trace ...` are **not** part of the canonical current CLI surface. +- Older flat core command concepts such as `specfact architecture ...` and `specfact trace ...` are **not** part of the canonical current CLI surface. The installed `nold-ai/specfact-requirements` bundle owns the canonical grouped `specfact requirements ...` surface. - The related OpenSpec changes now follow a split/rescope model: core owns schemas, contracts, and integration boundaries, while any future user-facing runtime delivery must land under canonical grouped bundle commands in `specfact-cli-modules`. +- Canonical bundle-deep documentation is owned by the [modules documentation site](https://modules.specfact.io/). - Protocol FSM modeling exists in data models; a full runtime FSM engine is still planned. ## Layer Model diff --git a/docs/contributing/docs-sync.md b/docs/contributing/docs-sync.md index c79ee0e0..49ddb6ae 100644 --- a/docs/contributing/docs-sync.md +++ b/docs/contributing/docs-sync.md @@ -10,7 +10,7 @@ doc_owner: specfact-cli tracks: - scripts/check_doc_frontmatter.py - docs/** -last_reviewed: 2026-03-29 +last_reviewed: 2026-07-10 exempt: false exempt_reason: "" --- @@ -50,6 +50,12 @@ Docs-only PRs rely on the dedicated `Docs Review` workflow as the required docum cross-site link check remains advisory because the published site can lag deployment, while frontmatter validation and docs test suites remain merge-blocking for docs-owned changes. +`hatch run check-documentation-accountability` is also blocking locally and in +PR CI. It reads the official module inventory from a local +`specfact-cli-modules` checkout (or `SPECFACT_MODULES_REPO`) and verifies that +core catalogues, generated command artifacts, and ownership statements agree. +It fails closed when that source is unavailable or inconsistent. + ## Troubleshooting - **Missing `doc_owner`**: add the field and a sensible `tracks` list for the code or specs this page describes. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 726891a1..288c3e2b 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -5,6 +5,11 @@ permalink: /getting-started/installation/ keywords: [install, setup, bootstrap, cli] audience: [solo, team] expertise_level: [beginner] +doc_owner: specfact-cli +tracks: [src/specfact_cli/modules/init/**, src/specfact_cli/registry/**, ../specfact-cli-modules/packages/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Getting Started with SpecFact CLI @@ -210,6 +215,12 @@ Category groups appear after bundle installation: - `specfact code ...` - `specfact spec ...` - `specfact govern ...` +- `specfact requirements ...` (after installing `nold-ai/specfact-requirements`) + +Official workflow package IDs are `nold-ai/specfact-project`, +`nold-ai/specfact-backlog`, `nold-ai/specfact-codebase`, +`nold-ai/specfact-code-review`, `nold-ai/specfact-spec`, +`nold-ai/specfact-govern`, and `nold-ai/specfact-requirements`. Backlog authentication commands such as `specfact backlog auth ...` are provided by the installed backlog bundle, not by the permanent core command surface. @@ -225,7 +236,11 @@ Legacy workflow presets remain available for bundle-first bootstrap: | `solo-developer` | `specfact-codebase`, `specfact-code-review` | `code` | | `backlog-team` | `specfact-project`, `specfact-backlog`, `specfact-codebase` | `project`, `backlog`, `code` | | `api-first-team` | `specfact-spec`, `specfact-codebase` (+`specfact-project` dependency) | `project`, `code`, `spec` | -| `enterprise-full-stack` | all five bundles | `project`, `backlog`, `code`, `spec`, `govern` | +| `enterprise-full-stack` | five profile-selected bundles | `project`, `backlog`, `code`, `spec`, `govern` | + +`nold-ai/specfact-requirements` is an official, opt-in evidence module rather +than a current profile default. Install it with `specfact init --install requirements`; +it adds `specfact requirements ...`. ### Upgrading from Pre-Slimming Versions diff --git a/docs/module-system/marketplace.md b/docs/module-system/marketplace.md index 145c60b2..03ba249d 100644 --- a/docs/module-system/marketplace.md +++ b/docs/module-system/marketplace.md @@ -9,6 +9,11 @@ description: Official SpecFact bundle IDs, trust tiers, and bundle dependency be keywords: [bundles, official modules, trust tiers] audience: [solo, team, enterprise] expertise_level: [intermediate] +doc_owner: specfact-cli +tracks: [src/specfact_cli/registry/**, ../specfact-cli-modules/packages/**, ../specfact-cli-modules/registry/index.json] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Marketplace Bundles @@ -28,6 +33,7 @@ Install commands: specfact module install nold-ai/specfact-project specfact module install nold-ai/specfact-backlog specfact module install nold-ai/specfact-codebase +specfact module install nold-ai/specfact-code-review specfact module install nold-ai/specfact-spec specfact module install nold-ai/specfact-govern specfact module install nold-ai/specfact-requirements @@ -38,6 +44,7 @@ Bundle overview: - `nold-ai/specfact-project`: grouped project workflows (`project`, `plan`, `import`, `sync`, `migrate`) - `nold-ai/specfact-backlog`: grouped backlog workflows (`backlog`, `policy`, `backlog auth`) - `nold-ai/specfact-codebase`: grouped code workflows (`analyze`, `drift`, `validate`, `repro`) +- `nold-ai/specfact-code-review`: scored clean-code and AI-bloat review workflows (`code review`) - `nold-ai/specfact-spec`: grouped spec workflows (`contract`, `api`, `sdd`, `generate`) - `nold-ai/specfact-govern`: grouped governance workflows (`enforce`, `patch`) - `nold-ai/specfact-requirements`: grouped requirements evidence workflows (`requirements`) diff --git a/docs/reference/commands.generated.json b/docs/reference/commands.generated.json index a444d190..6889d413 100644 --- a/docs/reference/commands.generated.json +++ b/docs/reference/commands.generated.json @@ -30,6 +30,7 @@ "init", "module", "project", + "requirements", "spec", "upgrade" ] @@ -46,7 +47,7 @@ "--show-completion" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [ @@ -94,7 +95,7 @@ "--type" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -115,7 +116,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -129,7 +130,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-backlog", "options": [], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [ @@ -151,7 +152,7 @@ "--use-device-code" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -167,7 +168,7 @@ "--provider" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -185,7 +186,7 @@ "--scopes" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -199,7 +200,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-backlog", "options": [], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -213,7 +214,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-backlog", "options": [], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [ @@ -235,7 +236,7 @@ "--mode" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -251,7 +252,7 @@ "--mode" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -267,7 +268,7 @@ "--mode" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -281,7 +282,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-backlog", "options": [], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -297,7 +298,7 @@ "--mode" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -350,7 +351,7 @@ "--yesterday" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -364,7 +365,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-backlog", "options": [], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [ @@ -388,7 +389,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -406,7 +407,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -425,7 +426,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -447,7 +448,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -466,7 +467,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -482,7 +483,7 @@ "--force" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -509,7 +510,7 @@ "--reset" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -529,7 +530,7 @@ "--to-status" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -582,7 +583,7 @@ "--write" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -603,7 +604,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -622,7 +623,7 @@ "--template" ], "owner_package": "nold-ai/specfact-backlog", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_backlog.backlog.commands:app", "subcommands": [] @@ -639,7 +640,7 @@ "--show-completion" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -660,7 +661,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-codebase", "options": [], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -679,7 +680,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -693,7 +694,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-codebase", "options": [], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -713,7 +714,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -742,7 +743,7 @@ "--shadow-only" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -767,7 +768,7 @@ "--write" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -796,7 +797,7 @@ "--shadow-only" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -821,7 +822,7 @@ "--verbose" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -840,7 +841,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -854,7 +855,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-code-review", "options": [], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [ @@ -872,7 +873,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-code-review", "options": [], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [ @@ -892,7 +893,7 @@ "--confirm" ], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -906,7 +907,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-code-review", "options": [], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -922,7 +923,7 @@ "--from" ], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -936,7 +937,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-code-review", "options": [], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [ @@ -956,7 +957,7 @@ "--ide" ], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -970,7 +971,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-code-review", "options": [], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -986,7 +987,7 @@ "--ide" ], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -1021,7 +1022,7 @@ "--with-mutation" ], "owner_package": "nold-ai/specfact-code-review", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_code_review.review.commands:app", "subcommands": [] @@ -1035,7 +1036,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-codebase", "options": [], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -1051,7 +1052,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-codebase", "options": [], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [ @@ -1068,7 +1069,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-codebase", "options": [], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -1087,7 +1088,7 @@ "--run-specmatic" ], "owner_package": "nold-ai/specfact-codebase", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_codebase.code.commands:app", "subcommands": [] @@ -1104,7 +1105,7 @@ "--show-completion" ], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [ @@ -1121,7 +1122,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-govern", "options": [], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [ @@ -1143,7 +1144,7 @@ "--sdd" ], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [] @@ -1159,7 +1160,7 @@ "--preset" ], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [] @@ -1173,7 +1174,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-govern", "options": [], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [ @@ -1193,7 +1194,7 @@ "--yes" ], "owner_package": "nold-ai/specfact-govern", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_govern.govern.commands:app", "subcommands": [] @@ -1566,7 +1567,7 @@ "--show-completion" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [ @@ -1605,7 +1606,7 @@ "--verbose" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1630,7 +1631,7 @@ "--template" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1650,7 +1651,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1670,7 +1671,7 @@ "--verbose" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1692,7 +1693,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1711,7 +1712,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1733,7 +1734,7 @@ "--template" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1753,7 +1754,7 @@ "--section" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1771,7 +1772,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1797,7 +1798,7 @@ "--theirs" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1818,7 +1819,7 @@ "--verbose" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1839,7 +1840,7 @@ "--resolution" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1859,7 +1860,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1873,7 +1874,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-project", "options": [], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [ @@ -1933,7 +1934,7 @@ "--watch" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1953,7 +1954,7 @@ "--watch" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1973,7 +1974,7 @@ "--watch" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -1992,7 +1993,7 @@ "--section" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -2006,7 +2007,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-project", "options": [], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [ @@ -2028,7 +2029,7 @@ "--type" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -2045,7 +2046,7 @@ "--repo" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] @@ -2063,11 +2064,104 @@ "--version" ], "owner_package": "nold-ai/specfact-project", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_project.project.commands:app", "subcommands": [] }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact requirements", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-requirements", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-requirements", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_requirements.requirements.commands:app", + "subcommands": [ + "coverage", + "import", + "list", + "validate" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact requirements coverage", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-requirements", + "options": [ + "--bundle", + "--format" + ], + "owner_package": "nold-ai/specfact-requirements", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_requirements.requirements.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact requirements import", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-requirements", + "options": [ + "--bundle", + "--format", + "--from-file" + ], + "owner_package": "nold-ai/specfact-requirements", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_requirements.requirements.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact requirements list", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-requirements", + "options": [ + "--bundle", + "--format", + "--show-coverage" + ], + "owner_package": "nold-ai/specfact-requirements", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_requirements.requirements.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact requirements validate", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-requirements", + "options": [ + "--bundle", + "--format", + "--profile" + ], + "owner_package": "nold-ai/specfact-requirements", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_requirements.requirements.commands:app", + "subcommands": [] + }, { "arguments": [], "bare_invocation": "requires-subcommand", @@ -2080,7 +2174,7 @@ "--show-completion" ], "owner_package": "nold-ai/specfact-spec", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_spec.spec.commands:app", "subcommands": [ @@ -2099,7 +2193,7 @@ "install_prerequisite": "specfact module install nold-ai/specfact-spec", "options": [], "owner_package": "nold-ai/specfact-spec", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_spec.spec.commands:app", "subcommands": [] @@ -2118,7 +2212,7 @@ "--output" ], "owner_package": "nold-ai/specfact-spec", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_spec.spec.commands:app", "subcommands": [] @@ -2139,7 +2233,7 @@ "--strict" ], "owner_package": "nold-ai/specfact-spec", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_spec.spec.commands:app", "subcommands": [] @@ -2158,7 +2252,7 @@ "--previous" ], "owner_package": "nold-ai/specfact-spec", - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli-modules", "short_help": "", "source": "specfact_spec.spec.commands:app", "subcommands": [] diff --git a/docs/reference/commands.generated.md b/docs/reference/commands.generated.md index 3199fa1f..61cce570 100644 --- a/docs/reference/commands.generated.md +++ b/docs/reference/commands.generated.md @@ -12,7 +12,7 @@ This file is generated from the current CLI command tree. Do not edit by hand. | Command | Owner | Options | Subcommands | Context | | --- | --- | --- | --- | --- | -| `specfact` | core | --banner, --debug, --input-format, --install-completion, --interactive, --mode, --no-interactive, --output-format, --show-completion, --skip-checks, --version; args: - | backlog, code, govern, init, module, project, spec, upgrade | | +| `specfact` | core | --banner, --debug, --input-format, --install-completion, --interactive, --mode, --no-interactive, --output-format, --show-completion, --skip-checks, --version; args: - | backlog, code, govern, init, module, project, requirements, spec, upgrade | | | `specfact backlog` | nold-ai/specfact-backlog | --install-completion, --show-completion; args: - | add, analyze-deps, auth, ceremony, daily, delta, diff, init-config, map-fields, promote, refine, sync, verify-readiness | | | `specfact backlog add` | nold-ai/specfact-backlog | --acceptance-criteria, --adapter, --body, --body-end-marker, --business-value, --check-dor, --custom-config, --description-format, --non-interactive, --parent, --priority, --project-id, --provider-field, --repo-path, --sprint, --story-points, --template, --title, --type; args: - | - | | | `specfact backlog analyze-deps` | nold-ai/specfact-backlog | --adapter, --custom-config, --json-export, --output, --project-id, --template; args: - | - | | @@ -113,6 +113,11 @@ This file is generated from the current CLI command tree. Do not edit by hand. | `specfact project version bump` | nold-ai/specfact-project | --bundle, --repo, --type; args: - | - | | | `specfact project version check` | nold-ai/specfact-project | --bundle, --repo; args: - | - | | | `specfact project version set` | nold-ai/specfact-project | --bundle, --repo, --version; args: - | - | | +| `specfact requirements` | nold-ai/specfact-requirements | --install-completion, --show-completion; args: - | coverage, import, list, validate | | +| `specfact requirements coverage` | nold-ai/specfact-requirements | --bundle, --format; args: - | - | | +| `specfact requirements import` | nold-ai/specfact-requirements | --bundle, --format, --from-file; args: - | - | | +| `specfact requirements list` | nold-ai/specfact-requirements | --bundle, --format, --show-coverage; args: - | - | | +| `specfact requirements validate` | nold-ai/specfact-requirements | --bundle, --format, --profile; args: - | - | | | `specfact spec` | nold-ai/specfact-spec | --install-completion, --show-completion; args: - | backward-compat, generate-tests, mock, validate | | | `specfact spec backward-compat` | nold-ai/specfact-spec | -; args: - | - | | | `specfact spec generate-tests` | nold-ai/specfact-spec | --bundle, --force, --out, --output; args: - | - | | diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 45220281..1c9334b7 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -5,6 +5,11 @@ permalink: /reference/commands/ keywords: [command reference, cli surface, command groups] audience: [solo, team, enterprise] expertise_level: [intermediate, advanced] +doc_owner: specfact-cli +tracks: [scripts/generate-command-overview.py, scripts/check-command-contract.py, ../specfact-cli-modules/packages/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Command Reference @@ -23,6 +28,7 @@ The live root command surface includes: - `specfact code ...` - `specfact govern ...` - `specfact project ...` +- `specfact requirements ...` (when `nold-ai/specfact-requirements` is installed) - `specfact spec ...` Use `specfact init --profile ` or `specfact init --install ` to bootstrap the workflow bundles you need. @@ -114,6 +120,13 @@ Compatibility note: `specfact backlog daily ...` and `specfact backlog refine .. - `code validate` - `code repro` +### `specfact requirements` (optional module) + +- `requirements import` +- `requirements list` +- `requirements coverage` +- `requirements validate` + ### `specfact spec` - `spec validate` @@ -132,7 +145,8 @@ Compatibility note: `specfact backlog daily ...` and `specfact backlog refine .. |---|---|---| | `nold-ai/specfact-project` | `project` | `project link-backlog`, `project health-check`, `project devops-flow`, `project snapshot`, `project regenerate`, `project export-roadmap`, `project import`, `project export`, `project sync`, `project version` | | `nold-ai/specfact-backlog` | `backlog` | `backlog ceremony`, `backlog refine`, `backlog daily`, `backlog sync`, `backlog auth`, `backlog analyze-deps`, `backlog verify-readiness`, `backlog delta`, `backlog add`, `backlog map-fields` | -| `nold-ai/specfact-codebase` | `code` | `code analyze`, `code drift`, `code validate`, `code repro`, `code import`, `code review` | +| `nold-ai/specfact-codebase` | `code` | `code analyze`, `code drift`, `code validate`, `code repro`, `code import` | +| `nold-ai/specfact-code-review` | `code review` | `code review run`, `code review rules`, `code review ledger` | | `nold-ai/specfact-spec` | `spec` | `spec validate`, `spec backward-compat`, `spec generate-tests`, `spec mock` | | `nold-ai/specfact-govern` | `govern` | `govern enforce`, `govern patch` | | `nold-ai/specfact-requirements` | `requirements` | `requirements import`, `requirements list`, `requirements coverage`, `requirements validate` | diff --git a/docs/reference/directory-structure.md b/docs/reference/directory-structure.md index aab6fedb..9b3ce0ea 100644 --- a/docs/reference/directory-structure.md +++ b/docs/reference/directory-structure.md @@ -4,6 +4,11 @@ title: SpecFact CLI Directory Structure permalink: /reference/directory-structure/ redirect_from: - /directory-structure/ +doc_owner: specfact-cli +tracks: [src/specfact_cli/**, ../specfact-cli-modules/packages/**, ../specfact-cli-modules/registry/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # SpecFact CLI Directory Structure @@ -74,14 +79,21 @@ specfact-cli-modules/ packages/specfact-project/ packages/specfact-backlog/ packages/specfact-codebase/ + packages/specfact-code-review/ packages/specfact-spec/ packages/specfact-govern/ + packages/specfact-requirements/ registry/ ``` The runtime loads bundle packages through manifests and registry metadata; it does not treat the source repositories themselves as runtime module roots. +Official marketplace package IDs are `nold-ai/specfact-project`, +`nold-ai/specfact-backlog`, `nold-ai/specfact-codebase`, +`nold-ai/specfact-code-review`, `nold-ai/specfact-spec`, +`nold-ai/specfact-govern`, and `nold-ai/specfact-requirements`. + ## Canonical Structure ```bash diff --git a/docs/reference/module-categories.md b/docs/reference/module-categories.md index 830dfe13..a63b6f98 100644 --- a/docs/reference/module-categories.md +++ b/docs/reference/module-categories.md @@ -3,6 +3,11 @@ layout: default title: Module Categories nav_order: 35 permalink: /reference/module-categories/ +doc_owner: specfact-cli +tracks: [src/specfact_cli/registry/**, ../specfact-cli-modules/packages/**] +last_reviewed: 2026-07-10 +exempt: false +exempt_reason: "" --- # Module Categories @@ -42,6 +47,7 @@ Category command groups: | `drift` | `codebase` | `specfact-codebase` | `code` | `drift` | | `validate` | `codebase` | `specfact-codebase` | `code` | `validate` | | `repro` | `codebase` | `specfact-codebase` | `code` | `repro` | +| `code_review` | `codebase` | `specfact-code-review` | `code` | `review` | | `contract` | `spec` | `specfact-spec` | `spec` | `contract` | | `spec` | `spec` | `specfact-spec` | `spec` | `api` | | `sdd` | `spec` | `specfact-spec` | `spec` | `sdd` | @@ -50,11 +56,17 @@ Category command groups: | `patch_mode` | `govern` | `specfact-govern` | `govern` | `patch` | | `requirements` | `requirements` | `specfact-requirements` | `requirements` | `requirements` | +Official marketplace package IDs are `nold-ai/specfact-project`, +`nold-ai/specfact-backlog`, `nold-ai/specfact-codebase`, +`nold-ai/specfact-code-review`, `nold-ai/specfact-spec`, +`nold-ai/specfact-govern`, and `nold-ai/specfact-requirements`. + ## Bundle Contents by Category - `specfact-project`: `project`, `plan`, `import`, `sync`, `migrate` - `specfact-backlog`: `backlog`, `policy` - `specfact-codebase`: `analyze`, `drift`, `validate`, `repro` +- `specfact-code-review`: `code review` - `specfact-spec`: `contract`, `api`, `sdd`, `generate` - `specfact-govern`: `enforce`, `patch` - `specfact-requirements`: `requirements` @@ -64,13 +76,14 @@ Category command groups: Official bundle packages are published from the dedicated modules repository: - Repository: `nold-ai/specfact-cli-modules` -- Package roots: `packages/specfact-project/`, `packages/specfact-backlog/`, `packages/specfact-codebase/`, `packages/specfact-spec/`, `packages/specfact-govern/`, `packages/specfact-requirements/` +- Package roots: `packages/specfact-project/`, `packages/specfact-backlog/`, `packages/specfact-codebase/`, `packages/specfact-code-review/`, `packages/specfact-spec/`, `packages/specfact-govern/`, `packages/specfact-requirements/` Namespace mapping: - `specfact-project` -> import namespace `specfact_project.*` - `specfact-backlog` -> import namespace `specfact_backlog.*` - `specfact-codebase` -> import namespace `specfact_codebase.*` +- `specfact-code-review` -> import namespace `specfact_code_review.*` - `specfact-spec` -> import namespace `specfact_spec.*` - `specfact-govern` -> import namespace `specfact_govern.*` - `specfact-requirements` -> import namespace `specfact_requirements.*` @@ -106,6 +119,10 @@ tier for config defaults while preserving their historical install selections: - `api-first-team` -> `mid_size` - `enterprise-full-stack` -> `enterprise` +`specfact-requirements` is an official, explicitly installable evidence module. +It remains outside the current named profile presets; install it with +`specfact init --install requirements` when requirements evidence is needed. + Examples: ```bash diff --git a/llms.txt b/llms.txt index 5a023302..48981793 100644 --- a/llms.txt +++ b/llms.txt @@ -14,7 +14,7 @@ This file is generated from the current CLI command tree. Do not edit by hand. | Command | Owner | Options | Subcommands | Context | | --- | --- | --- | --- | --- | -| `specfact` | core | --banner, --debug, --input-format, --install-completion, --interactive, --mode, --no-interactive, --output-format, --show-completion, --skip-checks, --version; args: - | backlog, code, govern, init, module, project, spec, upgrade | | +| `specfact` | core | --banner, --debug, --input-format, --install-completion, --interactive, --mode, --no-interactive, --output-format, --show-completion, --skip-checks, --version; args: - | backlog, code, govern, init, module, project, requirements, spec, upgrade | | | `specfact backlog` | nold-ai/specfact-backlog | --install-completion, --show-completion; args: - | add, analyze-deps, auth, ceremony, daily, delta, diff, init-config, map-fields, promote, refine, sync, verify-readiness | | | `specfact backlog add` | nold-ai/specfact-backlog | --acceptance-criteria, --adapter, --body, --body-end-marker, --business-value, --check-dor, --custom-config, --description-format, --non-interactive, --parent, --priority, --project-id, --provider-field, --repo-path, --sprint, --story-points, --template, --title, --type; args: - | - | | | `specfact backlog analyze-deps` | nold-ai/specfact-backlog | --adapter, --custom-config, --json-export, --output, --project-id, --template; args: - | - | | @@ -115,6 +115,11 @@ This file is generated from the current CLI command tree. Do not edit by hand. | `specfact project version bump` | nold-ai/specfact-project | --bundle, --repo, --type; args: - | - | | | `specfact project version check` | nold-ai/specfact-project | --bundle, --repo; args: - | - | | | `specfact project version set` | nold-ai/specfact-project | --bundle, --repo, --version; args: - | - | | +| `specfact requirements` | nold-ai/specfact-requirements | --install-completion, --show-completion; args: - | coverage, import, list, validate | | +| `specfact requirements coverage` | nold-ai/specfact-requirements | --bundle, --format; args: - | - | | +| `specfact requirements import` | nold-ai/specfact-requirements | --bundle, --format, --from-file; args: - | - | | +| `specfact requirements list` | nold-ai/specfact-requirements | --bundle, --format, --show-coverage; args: - | - | | +| `specfact requirements validate` | nold-ai/specfact-requirements | --bundle, --format, --profile; args: - | - | | | `specfact spec` | nold-ai/specfact-spec | --install-completion, --show-completion; args: - | backward-compat, generate-tests, mock, validate | | | `specfact spec backward-compat` | nold-ai/specfact-spec | -; args: - | - | | | `specfact spec generate-tests` | nold-ai/specfact-spec | --bundle, --force, --out, --output; args: - | - | | diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index 2906cf38..90dc013d 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -96,6 +96,7 @@ These changes make the CLI itself trustworthy enough to be the validation tool. |---:|---|---|---|---| | 1 | `cli-val-03-misuse-safety-proof` | [#281](https://github.com/nold-ai/specfact-cli/issues/281) | Misuse safety proof for user-facing commands | - | | 2 | `cli-val-04-acceptance-test-runner` | [#282](https://github.com/nold-ai/specfact-cli/issues/282) | Acceptance-test runner for CLI behavior proof | cli-val-03 | +| 3 | `cli-val-05-ci-integration` | [#643](https://github.com/nold-ai/specfact-cli/issues/643) | Fail-closed documentation accountability and CI validation enforcement | cli-val-02, cli-val-03, cli-val-04 | ## Modify queue before implementation diff --git a/openspec/changes/cli-val-05-ci-integration/CHANGE_VALIDATION.md b/openspec/changes/cli-val-05-ci-integration/CHANGE_VALIDATION.md new file mode 100644 index 00000000..e976a859 --- /dev/null +++ b/openspec/changes/cli-val-05-ci-integration/CHANGE_VALIDATION.md @@ -0,0 +1,50 @@ +# Change Validation: cli-val-05-ci-integration + +- **Validated on:** 2026-07-10 Europe/Berlin +- **Workflow:** implementation-readiness review and strict OpenSpec validation +- **Strict command:** `hatch run openspec validate cli-val-05-ci-integration --strict` +- **Result:** PASS (0 issues) + +## Scope Summary + +- **Extended capability:** `documentation-accountability` +- **Source issue:** [#643](https://github.com/nold-ai/specfact-cli/issues/643), + open and unblocked under parent [#375](https://github.com/nold-ai/specfact-cli/issues/375) +- **Outcome:** derive official module package and grouped-command ownership from + the paired modules repository; fail closed when core catalogues, generated + command metadata, or ownership statements drift. + +## Breaking-Change Analysis + +This is a validation and documentation hardening change. It adds no public CLI +command, removes no supported interface, and leaves the requirements profile +explicit-opt-in behavior unchanged. The only operational change is deliberate: +contributors and PRs must provide a valid modules checkout for documentation +validation rather than silently accepting incomplete documentation evidence. + +## Dependency and Ownership Review + +- `specfact-cli-modules` manifests and `registry/index.json` are the source of + truth for official package IDs and grouped command roots. +- Core owns the generated command overview, command contract, catalogue pages, + and the fail-closed validation integration. +- Modules own package manifests, marketplace registry entries, package runtime, + and deep module workflow documentation. +- The change order now lists `cli-val-05` with issue #643 and its validation + predecessors. + +## Internal Wiki Status + +The sibling internal checkout already has an uncommitted modification to +`wiki/sources/cli-val-05-ci-integration.md` outside this worktree. It was not +overwritten. Before merge, its owner must reconcile this scope update and run +`wiki_rebuild_graph.py` from the internal repository root; this preserves that +checkout's existing work while keeping the public OpenSpec source authoritative. + +## Validation Outcome + +- Required change artifacts are present and parseable. +- Strict OpenSpec validation passes. +- The change records source authority, fail-closed local/PR enforcement, + catalogue completeness, generated-command ownership, and architecture + handoff consistency. diff --git a/openspec/changes/cli-val-05-ci-integration/TDD_EVIDENCE.md b/openspec/changes/cli-val-05-ci-integration/TDD_EVIDENCE.md index 6c9a25cf..f1ce7ce3 100644 --- a/openspec/changes/cli-val-05-ci-integration/TDD_EVIDENCE.md +++ b/openspec/changes/cli-val-05-ci-integration/TDD_EVIDENCE.md @@ -1,5 +1,123 @@ # TDD Evidence: cli-val-05-ci-integration +## Documentation-accountability failing-before + +### 2026-07-10 Europe/Berlin + +Command: + +```bash +hatch run pytest tests/unit/docs/test_llms_overview_freshness.py tests/unit/docs/test_documentation_accountability.py -q +``` + +Result: failed as expected before implementation. + +Summary: + +- `docs/reference/commands.generated.json` has no `specfact requirements` + record owned by `nold-ai/specfact-requirements`. +- `scripts/check-documentation-accountability.py` does not exist. +- The always-run local pre-commit path and `docs-review.yml` do not invoke a + documentation-accountability command. +- Existing command-overview freshness coverage skipped in this worktree because + its modules-repository discovery does not find the documented sibling + checkout, demonstrating that it cannot be relied on as a fail-closed proof. + +## Documentation-accountability passing-after + +### 2026-07-10 Europe/Berlin + +Command: + +```bash +SPECFACT_MODULES_REPO=/Users/dom/git/nold-ai/specfact-cli-modules hatch run pytest tests/unit/docs/test_llms_overview_freshness.py tests/unit/docs/test_documentation_accountability.py -q +SPECFACT_MODULES_REPO=/Users/dom/git/nold-ai/specfact-cli-modules hatch run check-documentation-accountability +``` + +Result: passed after implementation. + +Summary: + +- 10 documentation-accountability tests passed. +- The generated command artifacts now include `specfact requirements` owned by + `nold-ai/specfact-requirements`. +- The accountability contract derives the seven official packages from the + modules manifests and marketplace registry, rejects disagreement or a missing + source, and confirms every designated core catalogue and ownership statement. +- The same fail-closed command is invoked by the always-run pre-commit path and + the blocking Docs Review PR workflow. + +## Documentation-accountability final quality evidence + +### 2026-07-10 Europe/Berlin + +Commands: + +```bash +SPECFACT_MODULES_REPO=/Users/dom/git/nold-ai/specfact-cli-modules hatch run pytest \ + tests/unit/docs/test_llms_overview_freshness.py \ + tests/unit/docs/test_documentation_accountability.py \ + tests/unit/registry/test_category_groups.py -q +SPECFACT_MODULES_REPO=/Users/dom/git/nold-ai/specfact-cli-modules hatch run docs-validate +hatch run type-check +hatch run lint +hatch run contract-test +hatch run smart-test-unit +hatch run smart-test-folder +hatch run smart-test-full +hatch run bandit-scan +``` + +Result: passed after remediation. + +Summary: + +- 20 focused tests passed. They prove catalogue omissions, missing generated + command records, and contradictory ownership claims fail for official module + roots, in addition to requirements inventory, missing-source coverage, + explicit-path failure, generated ownership metadata, and removed aliases. +- `smart-test-unit` and `smart-test-folder` completed with no changed paths to + select; `smart-test-full` ran the full suite. +- The full suite passed: 2,826 tests passed, 9 skipped, with 64% line + coverage against the configured and CI-enforced 50% `fail_under` gate. + The repository separately enforces contract coverage (80%) and behavioral + coverage (90%); this line-coverage result does not claim an 80% line target. +- The first full run exposed a stale category-group expectation that omitted + the already shipped `requirements` root; the test was corrected and the full + suite was rerun successfully. +- Type checking completed with 0 errors (existing repository warnings remain); + full lint, contract tests, workflow/YAML validation, Bandit, and strict + OpenSpec validation passed. + +## Clean-code review evidence and explicit exceptions + +### 2026-07-10 Europe/Berlin + +Command: + +```bash +bash scripts/pre-commit-quality-checks.sh +``` + +Result: passed. The staged hook ran the same fail-closed documentation gate, +command checks, lint, workflow checks, and code-review wrapper used before a +commit. + +- The review report has **0 errors** and `PASS_WITH_ADVISORY`. Its 12 warnings + are pre-existing findings on unchanged lines: the checker’s existing + catalogue-loop depth, legacy unknown-type findings, and pre-existing public + function decorator gaps in the command-overview generator. They are outside + this review-fix scope; no warning was introduced by these changes. +- All new-checker and changed-test findings were remediated: complexity, + nesting, output handling, type narrowing, design-by-contract coverage, and + duplicate test shape. +- 13 residual warnings are pre-existing static-analysis limitations in the + two legacy command-artifact scripts (`check-command-contract.py` and + `generate-command-overview.py`) plus Typer's dynamic `commands` attribute in + the existing category-group tests. They are non-blocking, produce no + type-check errors, and are outside this accountability scope; the report is + retained at `.specfact/code-review.json` for follow-up cleanup. + ## Failing-before ### 2026-06-13 00:00 Europe/Berlin diff --git a/openspec/changes/cli-val-05-ci-integration/design.md b/openspec/changes/cli-val-05-ci-integration/design.md index 71f18c71..da51c8ff 100644 --- a/openspec/changes/cli-val-05-ci-integration/design.md +++ b/openspec/changes/cli-val-05-ci-integration/design.md @@ -10,16 +10,20 @@ The hardening target is a PR pipeline that proves runtime behavior before merge - Treat Semgrep and Bandit as independent evidence: they run outside `specfact code review` and upload raw artifacts. - Keep Windows smoke scheduled/manual at first; make macOS PR-blocking for changed runtime/package paths. - Start mutation testing as scheduled advisory evidence, with explicit critical-module targeting and an uploaded baseline report. +- Treat the official modules repository as the authority for official package ids, root commands, and canonical deep-documentation ownership. Core must fail closed when that source cannot be resolved for a documentation-accountability run. +- Keep the accountability mapping explicit and reviewable: it names the core catalogue and ownership pages that must represent every official package, while the checker derives the required package and root-command set from the modules manifests and registry instead of duplicating it in Python. +- Run the same documentation-accountability command from pre-commit and the PR workflow. Generated command freshness, command syntax, frontmatter shape, and cross-site HTTP reachability remain complementary checks; none is a substitute for catalogue completeness and ownership consistency. ## Risks / Trade-offs - CI time increases. Mitigation: path-gate package matrix and macOS smoke; keep Windows scheduled until stable. - Wheel runtime checks may expose module checkout assumptions. Mitigation: use existing paired-branch module checkout logic and emit per-launcher logs. - Coverage enforcement can block useful work. Mitigation: use governed exception/profile work for temporary waivers, not silent advisory workflow behavior. +- A local contributor may not have the sibling modules checkout. Mitigation: resolve `SPECFACT_MODULES_REPO` first, then the documented sibling path; fail with an actionable setup message rather than silently skipping the accountability proof. ## Migration Plan 1. Add spec deltas and workflow policy tests. -2. Capture failing-before evidence for policy/property tests. -3. Implement workflow hardening, threshold enforcement, property tests, and mutation baseline job. -4. Validate with focused pytest, workflow lint/static checks, OpenSpec strict validation, and internal wiki graph rebuild. +2. Capture failing-before evidence for policy/property and documentation-accountability tests. +3. Implement workflow hardening, threshold enforcement, documentation accountability, property tests, and mutation baseline job. +4. Validate with focused pytest, pre-commit, workflow lint/static checks, OpenSpec strict validation, and internal wiki graph rebuild. diff --git a/openspec/changes/cli-val-05-ci-integration/proposal.md b/openspec/changes/cli-val-05-ci-integration/proposal.md index f8eafab4..6bf3b82c 100644 --- a/openspec/changes/cli-val-05-ci-integration/proposal.md +++ b/openspec/changes/cli-val-05-ci-integration/proposal.md @@ -13,6 +13,7 @@ Runtime validation only protects users when it runs before merge against the sam - **EXTEND**: Release PR fast-path logic so `dev -> main` parity skips only duplicate expensive tests, while release-safety checks still run. - **NEW**: Targeted cross-platform runtime smoke: macOS is PR-blocking for runtime/package paths; Windows starts on schedule/manual. - **NEW**: Property and mutation probes for dependency resolver, module installer, package parsing, marketplace selection, and upgrade/version logic. +- **NEW**: Fail-closed documentation-accountability gate that derives official module ownership from the modules repository, validates core catalogues and handoffs against it, and runs locally before commit and in required PR CI. ## Capabilities @@ -27,18 +28,19 @@ Runtime validation only protects users when it runs before merge against the sam - `ci-integration`: Package-manager runtime checks run before merge for affected pull requests. - `dependency-resolution`: Critical resolver and installer paths receive property-based regression coverage. - `codebase-validation-depth`: Mutation testing is introduced as scheduled/advisory evidence for critical validation code. +- `documentation-accountability`: Core documentation catalogues, generated command artifacts, and module ownership handoffs stay synchronized with the official modules repository. ## Impact -- **Affected specs**: `cli-validation-ci-gates`, `trustworthy-green-checks`, `ci-integration`, `dependency-resolution`, `codebase-validation-depth`, `acceptance-test-runner`. -- **Affected code**: GitHub Actions workflow, smart test coverage threshold handling, workflow policy tests, resolver property tests, mutation baseline configuration. -- **Integration points**: GitHub Actions, Hatch scripts, OpenSpec validation, internal wiki mirror, module checkout/runtime smoke fixtures. -- **Documentation impact**: CI and contributor guidance must distinguish blocking gates from advisory scheduled mutation evidence. +- **Affected specs**: `cli-validation-ci-gates`, `documentation-accountability`, `trustworthy-green-checks`, `ci-integration`, `dependency-resolution`, `codebase-validation-depth`, `acceptance-test-runner`. +- **Affected code**: GitHub Actions workflow, pre-commit quality gate, documentation-accountability checker, command overview/contract inventory, workflow policy tests, resolver property tests, mutation baseline configuration. +- **Integration points**: GitHub Actions, Hatch scripts, OpenSpec validation, the checked-out modules repository, internal wiki mirror, module checkout/runtime smoke fixtures. +- **Documentation impact**: Canonical core catalogues, architecture/ownership pages, and contributor guidance must identify every official package and distinguish core-owned overview material from modules-owned deep documentation. ## Source Tracking -- **GitHub Issue**: TBD -- **Issue URL**: TBD +- **GitHub Issue**: #643 +- **Issue URL**: - **Repository**: nold-ai/specfact-cli -- **Last Synced Status**: proposed +- **Last Synced Status**: open; unblocked; parent #375 CLI Behavior Validation Suite diff --git a/openspec/changes/cli-val-05-ci-integration/specs/documentation-accountability/spec.md b/openspec/changes/cli-val-05-ci-integration/specs/documentation-accountability/spec.md new file mode 100644 index 00000000..5eea203e --- /dev/null +++ b/openspec/changes/cli-val-05-ci-integration/specs/documentation-accountability/spec.md @@ -0,0 +1,74 @@ +## ADDED Requirements + +### Requirement: Official module documentation accountability SHALL derive from source authority + +The documentation-accountability contract SHALL derive the set of official +module package ids and grouped command roots from the checked-out +`specfact-cli-modules` manifests and marketplace registry. It SHALL reject a +missing, unreadable, or internally inconsistent modules source rather than +falling back to a hard-coded core package list. + +#### Scenario: Requirements module is an official source record + +- **GIVEN** the modules source contains the official + `nold-ai/specfact-requirements` manifest and registry entry +- **WHEN** the documentation-accountability contract builds its inventory +- **THEN** the inventory contains package id + `nold-ai/specfact-requirements` and root command `requirements` +- **AND** the generated command overview and runtime command contract include + that package and root. + +#### Scenario: Modules source is unavailable or inconsistent + +- **GIVEN** `SPECFACT_MODULES_REPO` and the documented sibling checkout cannot + provide a valid official-module manifest and registry source +- **WHEN** the documentation-accountability contract runs locally or in CI +- **THEN** it exits non-zero with an actionable source-resolution error +- **AND** it does not silently skip validation or reuse a stale hard-coded list. + +### Requirement: Core catalogues SHALL cover every official module + +The documentation-accountability contract SHALL require every designated core +package catalogue, command reference, install overview, and repository-layout +page to represent every official module package and its grouped command root +where that page describes command topology. The contract SHALL report the +missing package, expected root, file, and location. + +#### Scenario: A catalogue omits a shipped official module + +- **GIVEN** a designated core catalogue lists other official packages but omits + `nold-ai/specfact-requirements` +- **WHEN** the documentation-accountability contract runs +- **THEN** it exits non-zero +- **AND** identifies the catalogue file and omitted package. + +### Requirement: Core and modules documentation ownership SHALL be consistent + +Core overview and architecture pages SHALL accurately distinguish permanent +core runtime/lifecycle ownership from module-owned grouped command and deep +workflow documentation. They SHALL not classify an installed official grouped +command as a removed or non-canonical core surface, and required handoffs SHALL +target the canonical modules documentation path. + +#### Scenario: Core architecture contradicts an installed module command + +- **GIVEN** a core architecture or ownership page states that an official + grouped command such as `specfact requirements ...` is not canonical +- **WHEN** the documentation-accountability contract runs +- **THEN** it exits non-zero and identifies the conflicting ownership claim. + +### Requirement: Documentation accountability SHALL block locally and in PR CI + +The same documentation-accountability command SHALL run from the always-run +local pre-commit path and from a required pull-request workflow job whenever +documentation, command-contract, module-discovery, module manifest, or +workflow inputs change. + +#### Scenario: Accountability failure prevents delivery + +- **GIVEN** a staged or pull-request change leaves a catalogue, generated + command artifact, or ownership handoff inconsistent with the official-module + inventory +- **WHEN** pre-commit or PR CI runs +- **THEN** the relevant gate exits non-zero +- **AND** the pull request cannot satisfy its required documentation check. diff --git a/openspec/changes/cli-val-05-ci-integration/tasks.md b/openspec/changes/cli-val-05-ci-integration/tasks.md index e50c9adb..63c1ee1d 100644 --- a/openspec/changes/cli-val-05-ci-integration/tasks.md +++ b/openspec/changes/cli-val-05-ci-integration/tasks.md @@ -22,6 +22,7 @@ Per `openspec/config.yaml`, tests before code for any behavior-changing task. Or - [x] 2.1 Add spec deltas for CI gates, release parity, package runtime matrix, property tests, and mutation baseline. - [x] 2.2 Update `cli-val-04-acceptance-test-runner` design/spec to require built-wheel black-box execution. - [x] 2.3 Update internal wiki source pages for material scope changes and run `wiki_rebuild_graph.py`. +- [x] 2.4 Add a `documentation-accountability` spec delta covering source-of-truth derivation, catalogue completeness, ownership/handoff consistency, and fail-closed local/PR enforcement. ## 3. Test-first: workflow and correctness tests @@ -31,6 +32,8 @@ Per `openspec/config.yaml`, tests before code for any behavior-changing task. Or - [x] 3.4 Add Hypothesis property tests for dependency resolver, module installer dependency specs, version satisfaction, malformed manifests, registry identity helpers, and circular-dependency/self-reference surfaces. - [x] 3.5 Add upgrade/version-detection regression coverage for upgrade path selection, stale launcher repair, and metadata persistence failure handling. - [x] 3.6 Run focused dependency and upgrade/version tests and record failing-before output in `TDD_EVIDENCE.md`. +- [x] 3.7 Add failing tests proving a requirements-module omission, an incomplete generated command inventory, an incorrect core ownership claim, and an unavailable modules source each fail the accountability contract. +- [x] 3.8 Run the documentation-accountability tests before implementation and record the failing output in `TDD_EVIDENCE.md`. ## 4. Implementation @@ -38,19 +41,24 @@ Per `openspec/config.yaml`, tests before code for any behavior-changing task. Or - [x] 4.2 Update smart-test full-suite threshold handling so full runs enforce coverage threshold. - [x] 4.3 Add Hatch scripts/config for mutation baseline where needed. - [x] 4.4 Add or adjust helper code only where required by property tests. +- [x] 4.5 Implement an authoritative official-module inventory loader and documentation-accountability checker; remove the stale five-package command-mount assumptions from generated-command and command-contract validation. +- [x] 4.6 Run the accountability gate from the always-run local pre-commit path and the required PR CI path; make checkout/source-resolution failures blocking. +- [x] 4.7 Correct every identified core catalogue, generated command artifact, and architecture/ownership statement; preserve the current explicit-opt-in requirements profile behavior. ## 5. Passing evidence and quality gates - [x] 5.1 Re-run focused tests and record passing output in `TDD_EVIDENCE.md`. - [x] 5.2 Run `openspec validate cli-val-05-ci-integration --strict`. - [x] 5.3 Run workflow/policy tests for changed CI surfaces. -- [ ] 5.4 Run `hatch run type-check`, `hatch run lint`, `hatch run contract-test`, and `hatch run smart-test` as practical for the touched scope. -- [ ] 5.5 Run or document `hatch run specfact code review run --json --out .specfact/code-review.json` and resolve findings. +- [x] 5.4 Run `hatch run type-check`, `hatch run lint`, `hatch run contract-test`, and `hatch run smart-test` as practical for the touched scope. +- [x] 5.5 Run or document `hatch run specfact code review run --json --out .specfact/code-review.json` and resolve findings. +- [x] 5.6 Run the documentation-accountability command directly, through pre-commit, and through focused workflow-policy tests; record passing evidence in `TDD_EVIDENCE.md`. ## 6. Documentation and delivery - [x] 6.1 Update affected docs/README guidance for required vs advisory CI gates. -- [ ] 6.2 Update version/changelog if required by release policy. +- [x] 6.1.1 Update contributor documentation to explain the fail-closed documentation-accountability gate, its modules-repository prerequisite, and the core/modules ownership responsibilities. +- [x] 6.2 Update version/changelog if required by release policy. - [ ] 6.3 Stage and commit: `git add . && git commit -m "feat: harden PR runtime validation gates"` - [ ] 6.4 Push and create PR from `feature/cli-val-05-ci-integration` to `dev`. diff --git a/pyproject.toml b/pyproject.toml index 6daceee3..1d2b6661 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "specfact-cli" -version = "0.51.1" +version = "0.51.2" description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery." readme = "README.md" requires-python = ">=3.11" @@ -256,12 +256,13 @@ check-cross-site-links = "python scripts/check-cross-site-links.py" generate-command-overview = "python scripts/generate-command-overview.py --write" check-command-overview = "python scripts/generate-command-overview.py --check" check-command-contract = "python scripts/check-command-contract.py" +check-documentation-accountability = "python scripts/check-documentation-accountability.py" doc-frontmatter-check = "python scripts/check_doc_frontmatter.py" validate-agent-rule-signals = "python scripts/validate_agent_rule_applies_when.py" check-version-sources = "python scripts/check_version_sources.py" check-pypi-ahead = "python scripts/check_local_version_ahead_of_pypi.py" release = "python scripts/check_local_version_ahead_of_pypi.py && python scripts/check_version_sources.py" -docs-validate = "python scripts/generate-command-overview.py --check && python scripts/check-command-contract.py && python scripts/check-docs-commands.py && python scripts/check-cross-site-links.py --warn-only && python scripts/check_doc_frontmatter.py && python scripts/validate_agent_rule_applies_when.py" +docs-validate = "python scripts/generate-command-overview.py --check && python scripts/check-command-contract.py && python scripts/check-documentation-accountability.py && python scripts/check-docs-commands.py && python scripts/check-cross-site-links.py --warn-only && python scripts/check_doc_frontmatter.py && python scripts/validate_agent_rule_applies_when.py" # Legacy entry (kept for compatibility); prefer `workflows-lint` above lint-workflows = "bash scripts/run_actionlint.sh {args}" diff --git a/scripts/check-command-contract.py b/scripts/check-command-contract.py index abcd54ac..0ca37f4d 100644 --- a/scripts/check-command-contract.py +++ b/scripts/check-command-contract.py @@ -28,6 +28,7 @@ ("specfact_code_review.review.commands", "app", ("specfact", "code", "review")), ("specfact_govern.govern.commands", "app", ("specfact", "govern")), ("specfact_project.project.commands", "app", ("specfact", "project")), + ("specfact_requirements.requirements.commands", "app", ("specfact", "requirements")), ("specfact_spec.spec.commands", "app", ("specfact", "spec")), ) MISSING_MARKERS = ( @@ -245,10 +246,10 @@ def main(argv: list[str] | None = None) -> int: failures.extend(_check_missing_required_argument(runner, apps, record)) if failures: - print("Generated command contract validation failed:") - print("\n\n".join(failures)) + sys.stdout.write("Generated command contract validation failed:\n") + sys.stdout.write("\n\n".join(failures) + "\n") return 1 - print(f"check-command-contract: OK ({len(records)} generated command path(s) validated)") + sys.stdout.write(f"check-command-contract: OK ({len(records)} generated command path(s) validated)\n") return 0 diff --git a/scripts/check-documentation-accountability.py b/scripts/check-documentation-accountability.py new file mode 100644 index 00000000..b45ac8a2 --- /dev/null +++ b/scripts/check-documentation-accountability.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +"""Fail closed when core documentation drifts from official module ownership.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import cast + +import yaml +from beartype import beartype +from icontract import ensure, require + + +REPO_ROOT = Path(__file__).resolve().parents[1] +CATALOGUE_PATHS = ( + "README.md", + "docs/getting-started/installation.md", + "docs/module-system/marketplace.md", + "docs/reference/commands.md", + "docs/reference/module-categories.md", + "docs/reference/directory-structure.md", +) +OWNERSHIP_PATHS = ( + "docs/architecture/overview.md", + "docs/architecture/implementation-status.md", +) +COMMAND_CATALOGUE_PATHS = ( + "docs/getting-started/installation.md", + "docs/reference/commands.md", + "docs/reference/module-categories.md", +) + + +@dataclass(frozen=True) +class OfficialModule: + """Official module package identity and its public command roots.""" + + package_id: str + command_roots: tuple[str, ...] + + +def _require_modules_source(modules_root: Path) -> tuple[Path, Path]: + """Return required source paths or explain the incomplete checkout.""" + packages_path = modules_root / "packages" + registry_path = modules_root / "registry" / "index.json" + if not packages_path.is_dir() or not registry_path.is_file(): + raise ValueError(f"Modules source must contain packages/ and registry/index.json: {modules_root}") + return packages_path, registry_path + + +def _is_official_nold_module(data: Mapping[str, object]) -> bool: + """Return whether a manifest or registry record belongs to the official publisher.""" + publisher = data.get("publisher") + if not isinstance(publisher, Mapping): + return False + publisher_record = cast(Mapping[str, object], publisher) + return data.get("tier") == "official" and publisher_record.get("name") == "nold-ai" + + +def _read_yaml_mapping(path: Path) -> dict[str, object]: + """Read a manifest as a mapping or reject malformed official metadata.""" + data = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"Invalid module manifest: {path}") + return cast(dict[str, object], data) + + +def _official_manifest(path: Path) -> OfficialModule | None: + """Return one official manifest entry, ignoring third-party package metadata.""" + data = _read_yaml_mapping(path) + if not _is_official_nold_module(data): + return None + package_id = data.get("name") + commands = data.get("commands") + group_command = data.get("bundle_group_command") + if ( + not isinstance(package_id, str) + or not isinstance(commands, list) + or not all(isinstance(item, str) for item in commands) + ): + raise ValueError(f"Invalid official module manifest: {path}") + roots = (group_command,) if isinstance(group_command, str) and group_command else tuple(commands) + return OfficialModule(package_id, roots) + + +def _manifest_inventory(packages_path: Path) -> dict[str, OfficialModule]: + """Collect the source-authoritative official module records.""" + manifests: dict[str, OfficialModule] = {} + for path in sorted(packages_path.glob("*/module-package.yaml")): + module = _official_manifest(path) + if module is None: + continue + if module.package_id in manifests: + raise ValueError(f"Duplicate official module manifest: {module.package_id} ({path})") + manifests[module.package_id] = module + return manifests + + +def _registry_inventory(registry_path: Path) -> set[str]: + """Collect official package IDs from the marketplace registry.""" + registry = json.loads(registry_path.read_text(encoding="utf-8")) + if not isinstance(registry, dict): + raise ValueError(f"Invalid marketplace registry: {registry_path}") + entries = cast(dict[str, object], registry).get("modules") + if not isinstance(entries, list): + raise ValueError(f"Invalid marketplace registry: {registry_path}") + registry_ids: set[str] = set() + for item in entries: + if not isinstance(item, Mapping): + continue + record = cast(Mapping[str, object], item) + package_id = record.get("id") + if _is_official_nold_module(record) and isinstance(package_id, str): + registry_ids.add(package_id) + return registry_ids + + +@beartype +@require(lambda modules_root: modules_root is not None) +@ensure(lambda result: all(isinstance(package_id, str) for package_id in result)) +def discover_official_modules(modules_root: Path) -> dict[str, OfficialModule]: + """Load official packages from manifests and require registry agreement.""" + packages_path, registry_path = _require_modules_source(modules_root) + manifests = _manifest_inventory(packages_path) + registry_ids = _registry_inventory(registry_path) + if not manifests: + raise ValueError(f"No official module manifests found under {modules_root}") + if set(manifests) != registry_ids: + raise ValueError( + "Official module manifests and marketplace registry disagree: " + f"manifests={sorted(manifests)}, registry={sorted(registry_ids)}" + ) + return manifests + + +def _catalogue_findings(core_root: Path, package_ids: list[str]) -> list[str]: + """Return catalogue omissions for each official package.""" + findings: list[str] = [] + for relative_path in CATALOGUE_PATHS: + content = (core_root / relative_path).read_text(encoding="utf-8") + for package_id in package_ids: + if package_id not in content: + findings.append(f"{relative_path}: missing official package {package_id}") + return findings + + +def _command_catalogue_findings(core_root: Path, inventory: dict[str, OfficialModule]) -> list[str]: + """Require documented grouped roots wherever core documentation lists commands.""" + findings: list[str] = [] + for relative_path in COMMAND_CATALOGUE_PATHS: + content = (core_root / relative_path).read_text(encoding="utf-8") + for package_id, module in sorted(inventory.items()): + for root in module.command_roots: + if f"specfact {root}" not in content and f"`{root}`" not in content: + findings.append(f"{relative_path}: missing {package_id} command root {root}") + return findings + + +def _has_command_root(record: object, package_id: str, root: str) -> bool: + """Return whether one generated command record has the expected ownership.""" + if not isinstance(record, Mapping): + return False + command_record = cast(Mapping[str, object], record) + command = command_record.get("command") + return ( + command_record.get("owner_package") == package_id + and isinstance(command, str) + and command.split()[1:2] == [root] + ) + + +def _command_inventory_findings(core_root: Path, inventory: dict[str, OfficialModule]) -> list[str]: + """Return missing grouped roots from the generated command inventory.""" + generated_path = core_root / "docs/reference/commands.generated.json" + generated = json.loads(generated_path.read_text(encoding="utf-8")) + records = generated if isinstance(generated, list) else [] + findings: list[str] = [] + for package_id, module in sorted(inventory.items()): + for root in module.command_roots: + if not any(_has_command_root(record, package_id, root) for record in records): + findings.append(f"{generated_path.relative_to(core_root)}: missing {package_id} command root {root}") + return findings + + +def _ownership_conflicts(content: str, package_id: str, module: OfficialModule) -> list[str]: + """Return conflicting ownership claims for one official module.""" + return [ + package_id + for root in module.command_roots + if re.search(rf"\bspecfact\s+{re.escape(root)}\b[^\n]*\bnot\s+canonical\b", content, re.IGNORECASE) + ] + + +def _ownership_findings(core_root: Path, inventory: dict[str, OfficialModule]) -> list[str]: + """Return core ownership claims that contradict installed grouped commands.""" + findings: list[str] = [] + for relative_path in OWNERSHIP_PATHS: + content = (core_root / relative_path).read_text(encoding="utf-8") + if "https://modules.specfact.io/" not in content: + findings.append(f"{relative_path}: missing canonical modules documentation handoff") + for package_id, module in sorted(inventory.items()): + for conflicting_package in _ownership_conflicts(content, package_id, module): + findings.append( + f"{relative_path}: incorrectly rejects installed {conflicting_package} command ownership" + ) + return findings + + +@beartype +@require(lambda core_root, modules_root: core_root is not None and modules_root is not None) +@ensure(lambda result: all(isinstance(finding, str) for finding in result)) +def validate_documentation_accountability(core_root: Path, modules_root: Path) -> list[str]: + """Return deterministic ownership and catalogue findings for the core docs.""" + inventory = discover_official_modules(modules_root) + package_ids = sorted(inventory) + return [ + *_catalogue_findings(core_root, package_ids), + *_command_catalogue_findings(core_root, inventory), + *_command_inventory_findings(core_root, inventory), + *_ownership_findings(core_root, inventory), + ] + + +def _resolve_modules_root(raw_path: str | None) -> Path: + """Resolve the required modules checkout from explicit or documented paths.""" + if raw_path: + explicit = Path(raw_path).expanduser() + if (explicit / "packages").is_dir() and (explicit / "registry/index.json").is_file(): + return explicit.resolve() + raise ValueError(f"--modules-repo is not a valid modules checkout: {explicit}") + candidates: list[Path] = [] + configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip() + if configured: + candidates.append(Path(configured).expanduser()) + candidates.append(REPO_ROOT.parent / "specfact-cli-modules") + if "specfact-cli-worktrees" in REPO_ROOT.parts: + marker = REPO_ROOT.parts.index("specfact-cli-worktrees") + candidates.append(Path(*REPO_ROOT.parts[:marker]) / "specfact-cli-modules") + for candidate in candidates: + if (candidate / "packages").is_dir() and (candidate / "registry/index.json").is_file(): + return candidate.resolve() + raise ValueError( + "Cannot resolve specfact-cli-modules. Set SPECFACT_MODULES_REPO or clone it beside this repository." + ) + + +@beartype +@ensure(lambda result: isinstance(result, int)) +def main(argv: list[str] | None = None) -> int: + """Run the fail-closed documentation-accountability contract.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--modules-repo", help="Path to the specfact-cli-modules checkout") + args = parser.parse_args(argv) + try: + findings = validate_documentation_accountability(REPO_ROOT, _resolve_modules_root(args.modules_repo)) + except (OSError, ValueError, json.JSONDecodeError, yaml.YAMLError) as exc: + sys.stdout.write(f"documentation-accountability: {exc}\n") + return 1 + if findings: + sys.stdout.write("documentation-accountability: FAILED\n") + sys.stdout.write("\n".join(f"- {finding}" for finding in findings) + "\n") + return 1 + sys.stdout.write("documentation-accountability: OK\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate-command-overview.py b/scripts/generate-command-overview.py index 27518048..7172f19e 100644 --- a/scripts/generate-command-overview.py +++ b/scripts/generate-command-overview.py @@ -32,6 +32,12 @@ ("specfact_code_review.review.commands", "app", ("specfact", "code", "review"), "nold-ai/specfact-code-review"), ("specfact_govern.govern.commands", "app", ("specfact", "govern"), "nold-ai/specfact-govern"), ("specfact_project.project.commands", "app", ("specfact", "project"), "nold-ai/specfact-project"), + ( + "specfact_requirements.requirements.commands", + "app", + ("specfact", "requirements"), + "nold-ai/specfact-requirements", + ), ("specfact_spec.spec.commands", "app", ("specfact", "spec"), "nold-ai/specfact-spec"), ) @@ -186,7 +192,7 @@ def _walk( children = _command_children(command) record = { "command": " ".join(path), - "owner_repo": "nold-ai/specfact-cli", + "owner_repo": "nold-ai/specfact-cli" if owner_package == "core" else "nold-ai/specfact-cli-modules", "owner_package": owner_package, "install_prerequisite": install_prerequisite, "short_help": (command.short_help or "").strip(), @@ -357,9 +363,11 @@ def _check(outputs: dict[Path, str]) -> int: lineterm="", ) ) - print(diff) + sys.stdout.write(diff + "\n") if failures: - print("Command overview artifacts are stale. Run: python scripts/generate-command-overview.py --write") + sys.stdout.write( + "Command overview artifacts are stale. Run: python scripts/generate-command-overview.py --write\n" + ) return 1 return 0 diff --git a/scripts/pre-commit-quality-checks.sh b/scripts/pre-commit-quality-checks.sh index 57085c12..948ddb0c 100755 --- a/scripts/pre-commit-quality-checks.sh +++ b/scripts/pre-commit-quality-checks.sh @@ -422,7 +422,7 @@ run_command_overview_validation_gate() { while IFS= read -r file || [[ -n "${file}" ]]; do [[ -z "${file}" ]] && continue case "${file}" in - src/*|docs/*|.github/*|resources/*|scripts/check-docs-commands.py|scripts/check-command-contract.py|scripts/generate-command-overview.py|README.md|llms.txt|docs/reference/commands.generated.*) + src/*|docs/*|.github/*|resources/*|scripts/check-docs-commands.py|scripts/check-command-contract.py|scripts/check-documentation-accountability.py|scripts/generate-command-overview.py|README.md|llms.txt|docs/reference/commands.generated.*) hit=1 break ;; @@ -464,6 +464,13 @@ run_command_overview_validation_gate() { warn "💡 Fix stale command examples or regenerate the command overview if the CLI changed" exit 1 fi + info "📦 Block 2 — documentation accountability — running \`hatch run check-documentation-accountability\`" + if hatch run check-documentation-accountability; then + success "✅ Documentation accountability validation passed" + else + error "❌ Documentation accountability validation failed" + exit 1 + fi } run_contract_tests_visible() { diff --git a/setup.py b/setup.py index 1bb0b8f2..0460711c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ if __name__ == "__main__": _setup = setup( name="specfact-cli", - version="0.51.1", + version="0.51.2", description=( "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, " "and spec/contract evidence for AI-assisted and brownfield delivery." diff --git a/src/__init__.py b/src/__init__.py index 53dbf682..f5a09a31 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,4 +3,4 @@ """ # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py -__version__ = "0.51.1" +__version__ = "0.51.2" diff --git a/src/specfact_cli/__init__.py b/src/specfact_cli/__init__.py index bcb72fdd..d1fb49d4 100644 --- a/src/specfact_cli/__init__.py +++ b/src/specfact_cli/__init__.py @@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None: # keeps missing-command and missing-parameter UX consistent outside the root CLI too. _install_progressive_disclosure() -__version__ = "0.51.1" +__version__ = "0.51.2" __all__ = ["__version__"] diff --git a/tests/unit/docs/test_documentation_accountability.py b/tests/unit/docs/test_documentation_accountability.py new file mode 100644 index 00000000..0d4220f5 --- /dev/null +++ b/tests/unit/docs/test_documentation_accountability.py @@ -0,0 +1,220 @@ +"""Regression coverage for the fail-closed module documentation contract.""" + +from __future__ import annotations + +import importlib.util +import json +import os +import re +import shutil +import sys +from functools import lru_cache +from pathlib import Path +from typing import Protocol, cast + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class OfficialModuleRecord(Protocol): + """Read surface used by the checker regression tests.""" + + command_roots: tuple[str, ...] + + +class AccountabilityChecker(Protocol): + """Typed public surface loaded from the standalone checker script.""" + + CATALOGUE_PATHS: tuple[str, ...] + OWNERSHIP_PATHS: tuple[str, ...] + + def discover_official_modules(self, modules_root: Path) -> dict[str, OfficialModuleRecord]: + raise NotImplementedError + + def validate_documentation_accountability(self, core_root: Path, modules_root: Path) -> list[str]: + raise NotImplementedError + + def _resolve_modules_root(self, raw_path: str | None) -> Path: + raise NotImplementedError + + +def _modules_root() -> Path | None: + configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip() + candidates = [Path(configured).expanduser()] if configured else [] + candidates.append(REPO_ROOT.parent / "specfact-cli-modules") + if "specfact-cli-worktrees" in REPO_ROOT.parts: + marker = REPO_ROOT.parts.index("specfact-cli-worktrees") + candidates.append(Path(*REPO_ROOT.parts[:marker]) / "specfact-cli-modules") + suffix = Path(*REPO_ROOT.parts[marker + 1 :]) + candidates.append(Path(*REPO_ROOT.parts[:marker]) / "specfact-cli-modules-worktrees" / suffix) + return next((candidate for candidate in candidates if (candidate / "packages").is_dir()), None) + + +def test_modules_root_is_optional_without_a_paired_checkout(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Plain core checkouts skip external-fixture tests instead of raising StopIteration.""" + monkeypatch.delenv("SPECFACT_MODULES_REPO", raising=False) + monkeypatch.setattr(sys.modules[__name__], "REPO_ROOT", tmp_path) + + assert _modules_root() is None + + +@pytest.fixture(scope="module") +def modules_root() -> Path: + """Require the external modules fixture only for tests that exercise it.""" + resolved = _modules_root() + if resolved is None: + pytest.skip("specfact-cli-modules checkout is required for accountability contract tests") + return resolved + + +@lru_cache(maxsize=1) +def _load_accountability_checker() -> AccountabilityChecker: + path = REPO_ROOT / "scripts" / "check-documentation-accountability.py" + spec = importlib.util.spec_from_file_location("check_documentation_accountability", path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return cast(AccountabilityChecker, module) + + +def _copy_accountability_inputs(tmp_path: Path, checker: AccountabilityChecker) -> Path: + """Create a core-document fixture that starts from the checked-in contract.""" + for relative_path in (*checker.CATALOGUE_PATHS, *checker.OWNERSHIP_PATHS, "docs/reference/commands.generated.json"): + source = REPO_ROOT / relative_path + target = tmp_path / relative_path + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + return tmp_path + + +def test_accountability_uses_modules_source_for_official_requirements(modules_root: Path) -> None: + checker = _load_accountability_checker() + + inventory = checker.discover_official_modules(modules_root) + + assert inventory["nold-ai/specfact-requirements"].command_roots == ("requirements",) + + +def test_accountability_contract_accepts_current_catalogues(modules_root: Path) -> None: + checker = _load_accountability_checker() + + findings = checker.validate_documentation_accountability(REPO_ROOT, modules_root) + + assert findings == [] + + +def test_accountability_contract_reports_an_omitted_official_catalogue_package( + tmp_path: Path, modules_root: Path +) -> None: + checker = _load_accountability_checker() + core_root = _copy_accountability_inputs(tmp_path, checker) + readme = core_root / "README.md" + readme.write_text( + readme.read_text(encoding="utf-8").replace("specfact-requirements", "removed-module"), encoding="utf-8" + ) + + findings = checker.validate_documentation_accountability(core_root, modules_root) + + assert "README.md: missing official package nold-ai/specfact-requirements" in findings + + +def test_accountability_contract_reports_an_incomplete_generated_command_inventory( + tmp_path: Path, modules_root: Path +) -> None: + checker = _load_accountability_checker() + core_root = _copy_accountability_inputs(tmp_path, checker) + generated_path = core_root / "docs/reference/commands.generated.json" + records = json.loads(generated_path.read_text(encoding="utf-8")) + generated_path.write_text( + json.dumps( + [record for record in records if record.get("owner_package") != "nold-ai/specfact-requirements"], + indent=2, + ), + encoding="utf-8", + ) + + findings = checker.validate_documentation_accountability(core_root, modules_root) + + assert ( + "docs/reference/commands.generated.json: missing nold-ai/specfact-requirements command root requirements" + in findings + ) + + +def test_accountability_contract_reports_an_official_command_ownership_conflict( + tmp_path: Path, modules_root: Path +) -> None: + checker = _load_accountability_checker() + core_root = _copy_accountability_inputs(tmp_path, checker) + overview = core_root / "docs/architecture/overview.md" + overview.write_text( + f"{overview.read_text(encoding='utf-8')}\n\nspecfact code ... is not canonical\n", + encoding="utf-8", + ) + + findings = checker.validate_documentation_accountability(core_root, modules_root) + + assert ( + "docs/architecture/overview.md: incorrectly rejects installed nold-ai/specfact-code-review command ownership" + in findings + ) + + +def test_accountability_contract_detects_qualified_ownership_rejection(tmp_path: Path, modules_root: Path) -> None: + """Ownership rejections remain invalid when they include trailing qualifiers.""" + checker = _load_accountability_checker() + core_root = _copy_accountability_inputs(tmp_path, checker) + overview = core_root / "docs/architecture/overview.md" + overview.write_text( + f"{overview.read_text(encoding='utf-8')}\n\nspecfact code is not canonical delivery targets\n", + encoding="utf-8", + ) + + findings = checker.validate_documentation_accountability(core_root, modules_root) + + assert ( + "docs/architecture/overview.md: incorrectly rejects installed nold-ai/specfact-code-review command ownership" + in findings + ) + + +def test_accountability_contract_rejects_an_unavailable_modules_source(tmp_path: Path) -> None: + checker = _load_accountability_checker() + + with pytest.raises(ValueError, match="must contain packages"): + checker.discover_official_modules(tmp_path) + + +def test_explicit_invalid_modules_root_does_not_fall_back(tmp_path: Path, modules_root: Path) -> None: + """An explicit bad override must not silently use a different checkout.""" + checker = _load_accountability_checker() + + with pytest.raises(ValueError, match=re.escape(f"--modules-repo is not a valid modules checkout: {tmp_path}")): + checker._resolve_modules_root(str(tmp_path)) + + assert checker._resolve_modules_root(str(modules_root)) == modules_root.resolve() + + +def test_accountability_gate_is_mandatory_locally_and_in_pr_ci() -> None: + pre_commit = (REPO_ROOT / "scripts" / "pre-commit-quality-checks.sh").read_text(encoding="utf-8") + docs_workflow = yaml.safe_load( + (REPO_ROOT / ".github" / "workflows" / "docs-review.yml").read_text(encoding="utf-8") + ) + + gate_function = re.search( + r"run_command_overview_validation_gate\(\) \{(?P.*?)^\}", + pre_commit, + flags=re.MULTILINE | re.DOTALL, + ) + assert gate_function is not None + assert "if hatch run check-documentation-accountability; then" in gate_function.group("body") + assert "check-documentation-accountability ||" not in gate_function.group("body") + + steps = docs_workflow["jobs"]["docs-review"]["steps"] + accountability_step = next(step for step in steps if step.get("name") == "Validate documentation accountability") + assert accountability_step["run"] == "hatch run check-documentation-accountability" + assert accountability_step.get("continue-on-error", False) is False diff --git a/tests/unit/docs/test_llms_overview_freshness.py b/tests/unit/docs/test_llms_overview_freshness.py index 48a70b65..c1f5870c 100644 --- a/tests/unit/docs/test_llms_overview_freshness.py +++ b/tests/unit/docs/test_llms_overview_freshness.py @@ -8,6 +8,7 @@ from __future__ import annotations +import json import os import subprocess import sys @@ -25,6 +26,16 @@ ) +def _modules_repo_root() -> Path | None: + configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip() + candidates = [Path(configured).expanduser()] if configured else [] + candidates.append(REPO_ROOT.parent / "specfact-cli-modules") + if "specfact-cli-worktrees" in REPO_ROOT.parts: + marker = REPO_ROOT.parts.index("specfact-cli-worktrees") + candidates.append(Path(*REPO_ROOT.parts[:marker]) / "specfact-cli-modules") + return next((candidate for candidate in candidates if (candidate / "packages").is_dir()), None) + + def _paired_worktree_modules_repo() -> Path | None: """Mirror the generator's paired-worktree candidate (specfact-cli-worktrees layout).""" parts = REPO_ROOT.parts @@ -37,13 +48,10 @@ def _paired_worktree_modules_repo() -> Path | None: def _modules_repo_available() -> bool: - configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip() - candidates = [ - Path(configured).expanduser() if configured else None, - REPO_ROOT.parent / "specfact-cli-modules", - _paired_worktree_modules_repo(), - ] - return any(candidate is not None and (candidate / "packages").is_dir() for candidate in candidates) + if _modules_repo_root() is not None: + return True + paired = _paired_worktree_modules_repo() + return paired is not None and (paired / "packages").is_dir() def test_generated_command_artifacts_exist() -> None: @@ -51,17 +59,35 @@ def test_generated_command_artifacts_exist() -> None: assert (REPO_ROOT / relative).is_file(), f"Missing generated artifact: {relative}" +def test_generated_command_contract_covers_requirements_module() -> None: + """The official requirements package must be represented in generated docs.""" + records = json.loads((REPO_ROOT / "docs" / "reference" / "commands.generated.json").read_text(encoding="utf-8")) + + assert any( + record.get("command") == "specfact requirements" + and record.get("owner_package") == "nold-ai/specfact-requirements" + and record.get("owner_repo") == "nold-ai/specfact-cli-modules" + for record in records + ) + + def test_llms_and_command_overview_are_current() -> None: """llms.txt and the generated command reference must match the current CLI surface.""" if not _modules_repo_available(): pytest.skip("specfact-cli-modules packages checkout not available") + env = os.environ.copy() + modules_root = _modules_repo_root() + if modules_root is not None: + env["SPECFACT_MODULES_REPO"] = str(modules_root) + result = subprocess.run( [sys.executable, str(GENERATOR), "--check"], cwd=REPO_ROOT, capture_output=True, text=True, check=False, + env=env, timeout=300, ) assert result.returncode == 0, ( diff --git a/tests/unit/registry/test_category_groups.py b/tests/unit/registry/test_category_groups.py index fb6c1d33..490a7807 100644 --- a/tests/unit/registry/test_category_groups.py +++ b/tests/unit/registry/test_category_groups.py @@ -16,7 +16,7 @@ @pytest.fixture(autouse=True) -def _clear_registry() -> Generator[None, None, None]: +def clear_registry() -> Generator[None, None, None]: CommandRegistry._clear_for_testing() yield CommandRegistry._clear_for_testing() @@ -28,7 +28,7 @@ def test_bootstrap_with_category_grouping_enabled_registers_group_commands() -> register_builtin_commands() rebuild_root_app_from_registry() names = [name for name, _ in CommandRegistry.list_commands_for_help()] - allowed = {"init", "auth", "module", "upgrade", "code", "backlog", "project", "spec", "govern"} + allowed = {"init", "auth", "module", "upgrade", "code", "backlog", "project", "requirements", "spec", "govern"} forbidden_flat = { "analyze", "drift", @@ -46,6 +46,8 @@ def test_bootstrap_with_category_grouping_enabled_registers_group_commands() -> "patch", } assert set(names).issubset(allowed), f"Unexpected root commands found: {sorted(set(names) - allowed)}" + # Requirements is an opt-in module root: it is valid when installed, but + # fresh installs and compatibility jobs expose only permanent core roots. assert {"init", "module", "upgrade"}.issubset(set(names)) assert not (set(names) & forbidden_flat), ( f"Flat shims should not be registered: {sorted(set(names) & forbidden_flat)}" @@ -116,34 +118,12 @@ def test_govern_help_when_not_installed_suggests_install( ) -def test_flat_validate_is_not_found_in_copilot_mode( - tmp_path: Path, -) -> None: - """Flat `validate` is unavailable in copilot mode after shim removal.""" - with patch.dict( - os.environ, - {"SPECFACT_CATEGORY_GROUPING_ENABLED": "true", "SPECFACT_MODE": "copilot"}, - clear=False, - ): - register_builtin_commands() - rebuild_root_app_from_registry() - from click.testing import CliRunner - from typer.main import get_command - - from specfact_cli.cli import app - - runner = CliRunner() - root_cmd = get_command(app) - result = runner.invoke(cast(Any, root_cmd), ["validate", "--help"]) - assert result.exit_code != 0 - assert "not installed" in (result.output or "").lower() or "no such command" in (result.output or "").lower() - - -def test_flat_validate_is_not_found_in_cicd_mode(tmp_path: Path) -> None: - """Flat `validate` is unavailable in CI/CD mode after shim removal.""" +@pytest.mark.parametrize("mode", ("copilot", "cicd")) +def test_flat_validate_is_not_found_in_restricted_modes(tmp_path: Path, mode: str) -> None: + """Flat `validate` is unavailable in restricted modes after shim removal.""" with patch.dict( os.environ, - {"SPECFACT_CATEGORY_GROUPING_ENABLED": "true", "SPECFACT_MODE": "cicd"}, + {"SPECFACT_CATEGORY_GROUPING_ENABLED": "true", "SPECFACT_MODE": mode}, clear=False, ): register_builtin_commands() @@ -157,7 +137,9 @@ def test_flat_validate_is_not_found_in_cicd_mode(tmp_path: Path) -> None: root_cmd = get_command(app) result = runner.invoke(cast(Any, root_cmd), ["validate", "--help"]) assert result.exit_code != 0 - assert "not installed" in (result.output or "").lower() or "no such command" in (result.output or "").lower() + output = (result.output or "").lower() + assert "not installed" not in output + assert "no such command" in output or "removed" in output def test_spec_api_validate_routes_correctly(tmp_path: Path) -> None: