From 3fa1ab2ba2beb1dc5712da3afe3e4e16065a4def Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 8 Jul 2026 21:47:51 +0200 Subject: [PATCH 01/15] feat: add requirements runtime module --- docs/_data/nav.yml | 5 + docs/bundles/requirements/overview.md | 97 ++++++++++ docs/index.md | 1 + docs/reference/commands.generated.json | 93 ++++++++++ docs/reference/commands.generated.md | 5 + llms.txt | 5 + .../CHANGE_VALIDATION.md | 67 +++++-- .../TDD_EVIDENCE.md | 41 +++++ .../requirements-02-module-commands/design.md | 38 +++- .../proposal.md | 75 ++++++-- .../specs/backlog-adapter/spec.md | 18 +- .../specs/module-io-contract/spec.md | 22 ++- .../specs/requirements-module/spec.md | 53 +++--- .../requirements-02-module-commands/tasks.md | 32 ++-- .../specfact-requirements/module-package.yaml | 22 +++ .../src/specfact_requirements/__init__.py | 5 + .../requirements/__init__.py | 6 + .../specfact_requirements/requirements/app.py | 6 + .../requirements/commands.py | 138 +++++++++++++++ .../requirements/runtime.py | 165 ++++++++++++++++++ pyproject.toml | 4 + scripts/check-bundle-imports.py | 49 ++++-- scripts/check-command-contract.py | 40 +++-- scripts/check-docs-commands.py | 1 + scripts/check-prompt-commands.py | 1 + scripts/generate-command-overview.py | 24 ++- tests/conftest.py | 1 + .../test_command_apps.py | 92 ++++++++++ .../docs/test_bundle_overview_cli_examples.py | 21 +++ .../test_requirements_runtime.py | 89 ++++++++++ 30 files changed, 1090 insertions(+), 126 deletions(-) create mode 100644 docs/bundles/requirements/overview.md create mode 100644 openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md create mode 100644 packages/specfact-requirements/module-package.yaml create mode 100644 packages/specfact-requirements/src/specfact_requirements/__init__.py create mode 100644 packages/specfact-requirements/src/specfact_requirements/requirements/__init__.py create mode 100644 packages/specfact-requirements/src/specfact_requirements/requirements/app.py create mode 100644 packages/specfact-requirements/src/specfact_requirements/requirements/commands.py create mode 100644 packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py create mode 100644 tests/integration/specfact_requirements/test_command_apps.py create mode 100644 tests/unit/specfact_requirements/test_requirements_runtime.py diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml index 66f665c2..caa9704b 100644 --- a/docs/_data/nav.yml +++ b/docs/_data/nav.yml @@ -55,6 +55,11 @@ - title: Import & Migration url: /bundles/project/import-migration/ expertise: [intermediate, advanced] + - name: Requirements + items: + - title: Overview + url: /bundles/requirements/overview/ + expertise: [beginner, intermediate] - name: Codebase items: - title: Overview diff --git a/docs/bundles/requirements/overview.md b/docs/bundles/requirements/overview.md new file mode 100644 index 00000000..7a43e94f --- /dev/null +++ b/docs/bundles/requirements/overview.md @@ -0,0 +1,97 @@ +--- +layout: default +title: Requirements bundle overview +nav_order: 2 +permalink: /bundles/requirements/overview/ +keywords: [requirements, bundle, validation, evidence, coverage] +audience: [solo, team, enterprise] +expertise_level: [beginner, intermediate] +--- + +# Requirements bundle overview + +The **Requirements** bundle (`nold-ai/specfact-requirements`) imports existing +requirement context into SpecFact validation evidence. It normalizes upstream +records, stores them on project bundles as `requirements.inputs`, validates +evidence usefulness by profile, and reports coverage. It does not replace your +planning or product-management system. + +## Prerequisites + +- SpecFact CLI with core requirements context helpers +- Bundle installed: `specfact module install nold-ai/specfact-requirements` +- A project bundle directory, usually created by the [Project](/bundles/project/overview/) bundle +- Local JSON or YAML requirement records with source attribution + +## Command surface + +After installation, `specfact requirements --help` lists the runtime command +group. + +| Command | Purpose | +|--------|---------| +| `import` | Import local JSON/YAML requirement records into a project bundle | +| `validate` | Validate attached requirement context against a profile | +| `list` | List attached requirement records, optionally with coverage | +| `coverage` | Print coverage counts for downstream evidence links | + +## Input shape + +Import accepts either a list of records or a mapping with a `requirements` list. +Each record follows the core `RequirementInput` model and must include +`schema_version`, `requirement_id`, `title`, and at least one source reference. + +```json +{ + "requirements": [ + { + "schema_version": "1", + "requirement_id": "REQ-101", + "title": "Checkout requires fraud screening", + "sources": [ + { + "source_type": "issue", + "locator": "https://github.com/example/shop/issues/101" + } + ], + "evidence_links": [ + { + "link_type": "test", + "target": "tests/test_checkout_fraud.py" + } + ] + } + ] +} +``` + +## Quick examples + +```bash +specfact requirements import --from-file requirements.json --bundle .specfact/projects/shop --format json +specfact requirements list --bundle .specfact/projects/shop --show-coverage --format json +specfact requirements validate --bundle .specfact/projects/shop --profile enterprise --format json +specfact requirements coverage --bundle .specfact/projects/shop --format json +``` + +## Storage + +The command runtime rehydrates the core `requirements.inputs` extension before +delegating to validation helpers. Because current project bundle serialization +does not persist arbitrary extensions directly, the module also writes +`requirements.inputs.yaml` in the bundle root as the local persistence sidecar. + +## Scope boundaries + +The bundle is read-first and evidence-focused. + +- It imports and normalizes source-attributed requirement context. +- It reports validation findings and coverage gaps. +- It does not expose authoring templates. +- It does not perform bidirectional backlog sync or ceremony automation. + +## See also + +- [Project bundle overview](/bundles/project/overview/) +- [Backlog adapter patterns](/adapters/backlog-adapter-patterns/) +- [ProjectBundle schema](/reference/projectbundle-schema/) diff --git a/docs/index.md b/docs/index.md index 54fc43b2..45c85476 100644 --- a/docs/index.md +++ b/docs/index.md @@ -69,6 +69,7 @@ The modules site owns all bundle-specific deep guidance. Core CLI platform docs |--------|----------|------------| | Backlog | [Overview](bundles/backlog/overview/) | [Refinement](bundles/backlog/refinement/), [Delta](bundles/backlog/delta/), [Policy Engine](bundles/backlog/policy-engine/), [Dependency Analysis](bundles/backlog/dependency-analysis/) | | Project | [Overview](bundles/project/overview/) | [DevOps Flow](bundles/project/devops-flow/), [Import & Migration](bundles/project/import-migration/) | +| Requirements | [Overview](bundles/requirements/overview/) | Import, validate, list, coverage | | Codebase | [Overview](bundles/codebase/overview/) | [Sidecar Validation](bundles/codebase/sidecar-validation/), [Analyze](bundles/codebase/analyze/), [Drift](bundles/codebase/drift/), [Repro](bundles/codebase/repro/) | | Spec | [Overview](bundles/spec/overview/) | [Validate](bundles/spec/validate/), [Generate Tests](bundles/spec/generate-tests/), [Mock](bundles/spec/mock/) | | Govern | [Overview](bundles/govern/overview/) | [Enforce](bundles/govern/enforce/), [Patch](bundles/govern/patch/) | diff --git a/docs/reference/commands.generated.json b/docs/reference/commands.generated.json index c9e438de..21294e57 100644 --- a/docs/reference/commands.generated.json +++ b/docs/reference/commands.generated.json @@ -1695,6 +1695,99 @@ "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", diff --git a/docs/reference/commands.generated.md b/docs/reference/commands.generated.md index a3991d43..315987f9 100644 --- a/docs/reference/commands.generated.md +++ b/docs/reference/commands.generated.md @@ -93,6 +93,11 @@ This file is generated from the current module command trees. Do not edit by han | `specfact project version bump` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --type; args: - | - | | | `specfact project version check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo; args: - | - | | | `specfact project version set` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --version; args: - | - | | +| `specfact requirements` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --install-completion, --show-completion; args: - | coverage, import, list, validate | | +| `specfact requirements coverage` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format; args: - | - | | +| `specfact requirements import` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --from-file; args: - | - | | +| `specfact requirements list` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --show-coverage; args: - | - | | +| `specfact requirements validate` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --profile; args: - | - | | | `specfact spec` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --install-completion, --show-completion; args: - | backward-compat, generate-tests, mock, validate | | | `specfact spec backward-compat` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | -; args: - | - | | | `specfact spec generate-tests` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --out, --output; args: - | - | | diff --git a/llms.txt b/llms.txt index 9204718e..2cd332d2 100644 --- a/llms.txt +++ b/llms.txt @@ -97,6 +97,11 @@ This file is generated from the current module command trees. Do not edit by han | `specfact project version bump` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --type; args: - | - | | | `specfact project version check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo; args: - | - | | | `specfact project version set` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --version; args: - | - | | +| `specfact requirements` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --install-completion, --show-completion; args: - | coverage, import, list, validate | | +| `specfact requirements coverage` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format; args: - | - | | +| `specfact requirements import` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --from-file; args: - | - | | +| `specfact requirements list` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --show-coverage; args: - | - | | +| `specfact requirements validate` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --profile; args: - | - | | | `specfact spec` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --install-completion, --show-completion; args: - | backward-compat, generate-tests, mock, validate | | | `specfact spec backward-compat` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | -; args: - | - | | | `specfact spec generate-tests` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --out, --output; args: - | - | | diff --git a/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md b/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md index 4fb21c6d..d2719cf6 100644 --- a/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md +++ b/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md @@ -1,31 +1,64 @@ -# Change Validation: requirements-02-module-commands +# Change Validation Report: requirements-02-module-commands -- **Validated on (UTC):** 2026-02-15T21:54:26Z -- **Workflow:** /wf-validate-change (proposal-stage dry-run validation) +- **Validation Date (Europe/Berlin):** 2026-07-08T21:32:32+02:00 +- **Workflow:** OpenSpec validate-change refresh and implementation final gate - **Strict command:** `openspec validate requirements-02-module-commands --strict` - **Result:** PASS ## Scope Summary -- **New capabilities:** requirements-module -- **Modified capabilities:** module-io-contract,backlog-adapter -- **Declared dependencies:** requirements-01 (data model), arch-07 (#213, schema extensions for ProjectBundle) -- **Proposed affected code paths:** - `modules/requirements/` (new module);- `modules/backlog/src/adapters/` (extend adapters with AC text extraction interface) - `src/specfact_cli/contracts/module_interface.py` (no change — new implementation) +- **New capabilities:** requirements-validation-runtime +- **Modified capabilities:** module-io-contract, backlog-adapter +- **Declared dependencies:** core requirements input model and core + requirements context helpers from `nold-ai/specfact-cli#239` +- **Proposed affected code paths:** + - `packages/specfact-requirements/` + - `tests/unit/specfact_requirements/` + - `tests/integration/specfact_requirements/` + - `docs/bundles/requirements/` + - `docs/reference/commands.generated.*` + - `llms.txt` + - `scripts/check-bundle-imports.py` + - `scripts/generate-command-overview.py` -## Breaking-Change Analysis (Dry-Run) +## Breaking-Change Analysis -- Interface changes are proposal-level only; no production code modifications were performed in this workflow stage. -- Proposed modified capabilities are additive/extension-oriented in the current spec deltas and do not require immediate breaking migrations at proposal time. -- Backward-compatibility risk is primarily sequencing-related (dependency ordering), not signature-level breakage at this stage. +- The change adds a new module bundle and grouped command surface. +- Existing bundles and registry entries remain backward compatible. +- ProjectBundle integration remains optional through the existing + `requirements.inputs` extension namespace. +- No existing runtime command signature is changed. ## Dependency and Integration Review -- Dependency declarations align with the 2026-02-15 architecture layer integration plan sequencing. -- Cross-change integration points are explicitly represented in proposal/spec/task artifacts. -- No additional mandatory scope expansion was required to pass strict OpenSpec validation. +- Core `requirements-01-data-model` is implemented and archived. +- Core `requirements-02-module-commands` (#239) is paired parallel work and + exposes the helpers this module consumes. +- GitHub issue #165 was verified as open and not `in progress` through the + GitHub connector on 2026-07-08. +- The hierarchy cache refresh command succeeded with approved network access on + 2026-07-08T21:05:31+02:00. +- The connector does not expose GitHub project parent fields; the refreshed + local hierarchy cache remains the available structure evidence. ## Validation Outcome -- Required artifacts are present: `proposal.md`, `design.md`, `specs/**/*.md`, `tasks.md`. -- Strict OpenSpec validation passed. -- Change is ready for implementation-phase intake once prerequisites are satisfied. +- Required artifacts are present: `proposal.md`, `design.md`, `specs/**/*.md`, + `tasks.md`. +- Strict OpenSpec validation passed after implementation. +- Targeted failing-first test evidence was captured before the module package + existed, then passed after implementation. +- Final quality gates passed: + - `hatch run format` + - `hatch run type-check` + - `hatch run lint` + - `hatch run yaml-lint` + - `hatch run check-bundle-imports` + - `hatch run check-command-overview` + - `hatch run check-command-contract` + - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file /Users/dom/git/nold-ai/specfact-cli-worktrees/feature/requirements-02-module-commands/resources/keys/module-signing-public.pem` + - `hatch run contract-test` + - `hatch run smart-test` (`849 passed, 2 warnings`) + - `hatch run test` (`849 passed, 2 warnings`) + - `hatch run specfact code review run --enforcement changed --bug-hunt --json --out .specfact/code-review.json` +- SpecFact code review result: `PASS`, score `115`, `0` findings. diff --git a/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md b/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md new file mode 100644 index 00000000..c4089dd4 --- /dev/null +++ b/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md @@ -0,0 +1,41 @@ +# TDD Evidence: requirements-02-module-commands + +## Failing-before + +- **Timestamp (Europe/Berlin):** 2026-07-08T21:28:00+02:00 +- **Command:** `hatch run pytest tests/unit/specfact_requirements/test_requirements_runtime.py tests/integration/specfact_requirements/test_command_apps.py -q` +- **Result:** FAIL, expected +- **Summary:** Pytest failed during collection because the new requirements + runtime module did not exist yet. +- **Key error:** `ModuleNotFoundError: No module named 'specfact_requirements'` + +## Passing-after + +- **Timestamp (Europe/Berlin):** 2026-07-08T21:43:00+02:00 +- **Command:** `hatch run pytest tests/unit/specfact_requirements/test_requirements_runtime.py tests/integration/specfact_requirements/test_command_apps.py -q` +- **Result:** PASS +- **Summary:** 6 targeted tests passed, covering file import, bounded + diagnostics, sidecar-backed bundle persistence, profile-aware validation, + JSON command output, coverage inspection, and absence of an authoring command. + +## Quality Gates + +- **Timestamp (Europe/Berlin):** 2026-07-08T21:32:32+02:00 +- **Result:** PASS +- **Commands:** + - `openspec validate requirements-02-module-commands --strict` + - `hatch run format` + - `hatch run type-check` + - `hatch run lint` + - `hatch run yaml-lint` + - `hatch run check-bundle-imports` + - `hatch run check-command-overview` + - `hatch run check-command-contract` + - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file /Users/dom/git/nold-ai/specfact-cli-worktrees/feature/requirements-02-module-commands/resources/keys/module-signing-public.pem` + - `hatch run contract-test` + - `hatch run smart-test` + - `hatch run test` + - `hatch run specfact code review run --enforcement changed --bug-hunt --json --out .specfact/code-review.json` +- **Summary:** targeted and full gates passed after the requirements runtime + cleanup. `smart-test` and `test` both reported `849 passed, 2 warnings`. + SpecFact code review reported `PASS`, score `115`, and no findings. diff --git a/openspec/changes/requirements-02-module-commands/design.md b/openspec/changes/requirements-02-module-commands/design.md index f70d8055..906c3814 100644 --- a/openspec/changes/requirements-02-module-commands/design.md +++ b/openspec/changes/requirements-02-module-commands/design.md @@ -1,6 +1,10 @@ ## Context -This change implements proposal scope for `requirements-02-module-commands` from the 2026-02-15 architecture-layer integration plan. It is proposal-stage only and defines implementation strategy without changing runtime code. +This change implements the module-owned runtime scope for +`requirements-02-module-commands` from the 2026-02-15 architecture-layer +integration plan. The paired core change `nold-ai/specfact-cli#239` supplies +shared requirement context helpers; this modules change supplies the grouped +runtime command surface. ## Goals / Non-Goals @@ -8,9 +12,14 @@ This change implements proposal scope for `requirements-02-module-commands` from - Define an implementation approach that stays within the proposal scope. - Keep compatibility with existing module registry, adapter bridge, and contract-first patterns. - Preserve offline-first behavior and deterministic CLI execution. +- Provide `specfact requirements ...` command handlers that reuse core + normalization, bundle extension IO, validation report, and coverage summary + helpers. +- Emit JSON-friendly command output for validation evidence and AI handoff. **Non-Goals:** -- No production code implementation in this stage. +- No requirement authoring workflow. +- No bidirectional backlog sync or ceremony automation. - No schema-breaking changes outside declared capabilities. - No dependency expansion beyond the proposal and plan. @@ -20,21 +29,38 @@ This change implements proposal scope for `requirements-02-module-commands` from - Keep all public APIs contract-first with `@icontract` and `@beartype`. - Make all behavior extensions opt-in or backward-compatible by default. - Add/modify OpenSpec deltas first so tests can be derived before implementation. +- Store normalized requirement context through the existing + `requirements.inputs` extension from `requirements-01-data-model`. +- Keep command output bounded to core diagnostics, validation reports, and + coverage summaries. +- Treat failed validation as a non-zero command result while keeping `warnings` + visible as successful advisory evidence. ## Risks / Trade-offs - [Dependency ordering drift] -> Mitigation: gate implementation tasks on declared prerequisites. - [Capability overlap with adjacent changes] -> Mitigation: keep this change scoped to listed capabilities only. - [Documentation drift] -> Mitigation: include explicit docs update tasks in apply phase. +- [Core helper drift] -> Mitigation: validate against the paired + `specfact-cli-worktrees/feature/requirements-02-module-commands` checkout + until core #239 merges. +- [Command ownership confusion] -> Mitigation: module ships runtime command + handlers; core only provides helpers and missing-module diagnostics. ## Migration Plan -1. Implement this change only after listed dependencies are implemented. +1. Implement this change only after listed dependencies are implemented or + accepted as paired parallel work. 2. Add tests from spec scenarios and capture failing-first evidence. 3. Implement minimal production changes needed for passing scenarios. -4. Run quality gates and then open PR to `dev`. +4. Update public docs, command overview artifacts, issue body, and wiki mirror + metadata. +5. Run quality gates and then open PR to `dev`. ## Open Questions -- Dependency summary: Depends on requirements-01-data-model and arch-07-schema-extension-system. -- Whether additional cross-change sequencing constraints should be hard-blocked in `openspec/CHANGE_ORDER.md`. +- Dependency summary: `requirements-01-data-model` and + `arch-07-schema-extension-system` are implemented in core; core #239 is paired + parallel work and must remain API-compatible with this module runtime. +- The connector does not expose GitHub project fields; local hierarchy cache and + live issue state remain the available governance evidence. diff --git a/openspec/changes/requirements-02-module-commands/proposal.md b/openspec/changes/requirements-02-module-commands/proposal.md index 222a1425..7dba75f5 100644 --- a/openspec/changes/requirements-02-module-commands/proposal.md +++ b/openspec/changes/requirements-02-module-commands/proposal.md @@ -1,28 +1,36 @@ -# Change: Requirements Import and Validation Runtime +# Change: Requirements Runtime Commands ## Why -SpecFact needs module commands that import, normalize, validate, and inspect -upstream requirement context for evidence. It should not become the authoring -stack for requirements. +SpecFact needs a module-owned `requirements` command group that imports, +normalizes, validates, and inspects upstream requirement context for validation +evidence. It should not become the authoring stack for requirements, since +teams may already use Spec Kit, OpenSpec, Jira, GitHub Issues, Azure DevOps, +Linear, documents, or another planning source. ## Ownership Alignment (2026-06-06) -- Modules-owned scope retained here: grouped command runtime, adapters, and - validation behavior for normalized requirement inputs. -- Core-owned scope remains the shared requirements input model and evidence - contracts. +- Repository assignment: `split/rescope` +- Modules-owned scope retained here: grouped `specfact requirements ...` + runtime commands, module manifest wiring, docs, and adapter-facing command + behavior for normalized requirement inputs. +- Core-owned scope remains the shared requirements input model, adapter helper + APIs, evidence contracts, and missing-module root diagnostics. +- Runtime commands MUST consume the paired core helpers from + `nold-ai/specfact-cli#239`. - Requirement authoring templates are no longer critical-path scope. ## What Changes -- **NEW**: Import commands for backlog items, OpenSpec proposals, Spec Kit feature - folders, and local requirement records. -- **NEW**: Normalization into source-attributed records compatible with the core - requirements input model. -- **NEW**: Validation and coverage inspection for evidence usefulness by profile. -- **NEW**: Adapter hooks return bounded records instead of free-form planning - prose. +- **NEW**: `specfact requirements import` command for local requirement records + and adapter-produced source-attributed records. +- **NEW**: `specfact requirements validate` command that delegates to the core + profile-aware validation boundary. +- **NEW**: `specfact requirements list` and `specfact requirements coverage` + commands for machine-readable coverage inspection. +- **NEW**: Requirements module manifest and command overview wiring. +- **NEW**: Command runtime preserves bounded core diagnostics instead of + generating free-form planning prose. - **REMOVED FROM CRITICAL PATH**: Interactive requirement authoring and full requirement lifecycle management. @@ -30,16 +38,44 @@ stack for requirements. ### New Capabilities -- `requirements-validation-runtime`: Runtime commands for importing, +- `requirements-validation-runtime`: Module runtime commands for importing, normalizing, validating, and inspecting upstream requirement context. ### Modified Capabilities -- `module-io-contract`: Requirements implementation focuses on import and - validation hooks for evidence. +- `module-io-contract`: Requirements implementation focuses on import, + validation, and coverage hooks for evidence. - `backlog-adapter`: Backlog adapters can provide source-attributed requirement snippets. +## Impact + +- **Affected specs**: `requirements-module`, `module-io-contract`, + `backlog-adapter` +- **Affected code**: + - `packages/specfact-requirements/module-package.yaml` + - `packages/specfact-requirements/src/specfact_requirements/requirements/commands.py` + - `packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py` + - `scripts/generate-command-overview.py` + - `scripts/check-bundle-imports.py` + - `tests/conftest.py` +- **Affected tests**: + - `tests/unit/specfact_requirements/test_requirements_runtime.py` + - `tests/integration/specfact_requirements/test_command_apps.py` +- **Affected docs**: + - `docs/bundles/requirements/overview.md` + - `docs/reference/commands.generated.json` + - `docs/reference/commands.generated.md` + - `docs/_data/nav.yml` + - `llms.txt` +- **Integration points**: Consumes the core helper APIs from + `nold-ai/specfact-cli#239` and the `requirements.inputs` model from + `requirements-01-data-model`. +- **Rollback plan**: remove the requirements bundle package, generated command + overview rows, docs/nav entries, tests, and module manifest. Existing + `requirements.inputs` bundle data remains compatible because the data model is + core-owned. + --- ## Source Tracking @@ -48,5 +84,6 @@ stack for requirements. - **GitHub Issue**: #165 - **Issue URL**: - **Core Counterpart**: nold-ai/specfact-cli#239 -- **Last Synced Status**: proposed +- **Last Synced Status**: in_progress - **Sanitized**: false + diff --git a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md b/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md index 9a401ffa..8292f8eb 100644 --- a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md +++ b/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md @@ -1,16 +1,20 @@ ## MODIFIED Requirements ### Requirement: Backlog Adapter -The system SHALL expose backlog acceptance-criteria content to requirements extraction workflows. + +The system SHALL expose source-attributed backlog requirement snippets to +requirements import workflows. #### Scenario: Adapter returns acceptance criteria payload for extraction -- **GIVEN** a backlog item selected for extraction -- **WHEN** requirements extraction requests source fields -- **THEN** adapter returns title, description, acceptance-criteria text, and item identity -- **AND** extraction proceeds without provider-specific parsing in command handlers. + +- **GIVEN** a backlog item selected for requirement context import +- **WHEN** requirements import receives adapter source fields +- **THEN** title, description, acceptance-criteria text, and item identity are available +- **AND** normalization proceeds without provider-specific parsing in command handlers. #### Scenario: Missing acceptance criteria is surfaced explicitly + - **GIVEN** a backlog item with no acceptance criteria -- **WHEN** extraction runs -- **THEN** item is reported as incomplete input +- **WHEN** normalization runs +- **THEN** the item is reported as incomplete input - **AND** command output includes the backlog item identifier. diff --git a/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md b/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md index 3a4e15ee..28f851e8 100644 --- a/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md +++ b/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md @@ -1,16 +1,20 @@ ## MODIFIED Requirements ### Requirement: Module Io Contract -The requirements module SHALL implement all `ModuleIOContract` operations. + +The requirements module SHALL consume core requirements context adapter helpers +through the existing `ModuleIOContract` boundary. #### Scenario: Import operation maps backlog items to requirements -- **GIVEN** backlog input for import -- **WHEN** `import_to_bundle` runs -- **THEN** requirements are added to the bundle with stable IDs -- **AND** parse diagnostics are included for partial failures. + +- **GIVEN** source-attributed requirement records imported by a module command +- **WHEN** the runtime stores normalized requirements on a `ProjectBundle` +- **THEN** requirements are added under the `requirements.inputs` extension with stable IDs +- **AND** parse diagnostics remain available to the module runtime for partial failures. #### Scenario: Validate operation enforces profile schema -- **GIVEN** requirements bundle and active profile schema -- **WHEN** `validate_bundle` runs -- **THEN** missing required fields are reported -- **AND** validation severity respects active policy mode. + +- **GIVEN** a requirements bundle and active validation profile +- **WHEN** the runtime delegates to core requirements context validation +- **THEN** missing evidence links and weak context are reported +- **AND** validation severity respects the selected evidence strictness profile. diff --git a/openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md b/openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md index 769f2813..dad621a7 100644 --- a/openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md +++ b/openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md @@ -1,22 +1,35 @@ ## ADDED Requirements -### Requirement: Requirements Module -The system SHALL provide requirements CLI commands for extract, author, validate, and list. - -#### Scenario: Extract command creates requirement artifacts -- **GIVEN** `specfact requirements extract --from-backlog --output .specfact/requirements/` -- **WHEN** extraction succeeds -- **THEN** one or more `*.req.yaml` files are produced -- **AND** each file includes schema version and source backlog reference. - -#### Scenario: Author command applies profile-aware template fields -- **GIVEN** `specfact requirements author --template story` -- **WHEN** active profile is `solo` -- **THEN** authoring prompts require only solo-required fields -- **AND** optional advanced fields remain non-blocking. - -#### Scenario: Validate and list expose completeness and trace coverage -- **GIVEN** requirement artifacts with trace references -- **WHEN** `specfact requirements validate` and `specfact requirements list --show-coverage` run -- **THEN** completeness and coverage are reported per requirement -- **AND** output is machine-readable when requested. +### Requirement: Requirements Runtime Commands + +The system SHALL provide module-owned `specfact requirements ...` commands for +importing, validating, listing, and inspecting upstream requirement context as +validation evidence. + +#### Scenario: Import local requirement records into bundle extensions + +- **GIVEN** a local JSON or YAML file containing source-attributed requirement records +- **WHEN** `specfact requirements import --from-file --bundle ` runs +- **THEN** valid records are stored under the bundle's `requirements.inputs` extension +- **AND** invalid records are returned as bounded diagnostics. + +#### Scenario: Validate requirement context by profile + +- **GIVEN** a project bundle with normalized requirement inputs +- **WHEN** `specfact requirements validate --bundle --profile enterprise` runs +- **THEN** validation delegates to the core profile-aware requirements context helper +- **AND** missing downstream evidence links are reported as failed validation. + +#### Scenario: List requirements with coverage summary + +- **GIVEN** requirement inputs are present on a project bundle +- **WHEN** `specfact requirements list --bundle --show-coverage --format json` runs +- **THEN** each requirement ID and title is returned +- **AND** the output includes a machine-readable coverage summary. + +#### Scenario: No authoring command is exposed + +- **GIVEN** the requirements module is installed +- **WHEN** the user inspects `specfact requirements --help` +- **THEN** import, validate, list, and coverage commands are visible +- **AND** requirement authoring commands are not exposed. diff --git a/openspec/changes/requirements-02-module-commands/tasks.md b/openspec/changes/requirements-02-module-commands/tasks.md index 592c1906..ca1ed0d5 100644 --- a/openspec/changes/requirements-02-module-commands/tasks.md +++ b/openspec/changes/requirements-02-module-commands/tasks.md @@ -2,29 +2,35 @@ ## 1. Branch and dependency guardrails -- [ ] 1.1 Create dedicated worktree branch `feature/requirements-02-module-commands` from `dev` before implementation work: `scripts/worktree.sh create feature/requirements-02-module-commands`. -- [ ] 1.2 Verify prerequisite changes are implemented or explicitly accepted as parallel work. -- [ ] 1.3 Reconfirm scope against the 2026-02-15 architecture integration plan and this proposal. +- [x] 1.1 Create dedicated worktree branch `feature/requirements-02-module-commands` from `dev` before implementation work. +- [x] 1.2 Refresh GitHub hierarchy cache, verify issue #165 is not in progress, and confirm available label/structure metadata. +- [x] 1.3 Verify prerequisite changes are implemented or explicitly accepted as paired parallel work. +- [x] 1.4 Reconfirm scope against `openspec/CHANGE_ORDER.md`: keep this change as module runtime commands for import, normalization, validation, and coverage evidence. +- [x] 1.5 Update the public GitHub issue body to match the narrowed validation-evidence format. +- [x] 1.6 Update the wiki mirror and run the graph rebuild workflow if a mirror source exists. ## 2. Spec-first and test-first preparation -- [ ] 2.1 Finalize `specs/` deltas for all listed capabilities and cross-check scenario completeness. -- [ ] 2.2 Add/update tests mapped to new and modified scenarios. -- [ ] 2.3 Run targeted tests to capture failing-first behavior and record results in `TDD_EVIDENCE.md`. +- [x] 2.1 Finalize `specs/` deltas for all listed capabilities and cross-check scenario completeness. +- [x] 2.2 Add/update tests mapped to new and modified scenarios. +- [x] 2.3 Run targeted tests to capture failing-first behavior and record results in `TDD_EVIDENCE.md`. ## 3. Implementation -- [ ] 3.1 Implement minimal production code required to satisfy the new scenarios. -- [ ] 3.2 Add/update contract decorators and type enforcement on public APIs. -- [ ] 3.3 Update command wiring, adapters, and models required by this change scope only. +- [x] 3.1 Implement `specfact-requirements` module package and grouped command app. +- [x] 3.2 Add/update contract decorators and type enforcement on public APIs. +- [x] 3.3 Update command overview, import-boundary wiring, module manifest, and test bootstrap source lists. +- [x] 3.4 Keep requirement authoring, backlog write-back, and lifecycle management outside this module runtime. ## 4. Validation and documentation -- [ ] 4.1 Re-run tests and quality gates until all changed scenarios pass. -- [ ] 4.2 Update user-facing docs and navigation for changed/added commands and workflows. -- [ ] 4.3 Run `openspec validate requirements-02-module-commands --strict` and resolve all issues. +- [x] 4.1 Re-run tests and quality gates until all changed scenarios pass. +- [x] 4.2 Update user-facing docs and navigation for changed/added commands and workflows. +- [x] 4.3 Run module-signature verification; if signed module assets changed, bump module versions and re-sign before PR. +- [x] 4.4 Run `openspec validate requirements-02-module-commands --strict` and resolve all issues. +- [x] 4.5 Run SpecFact code review JSON and clean-code gates; resolve all findings or document rare explicit exceptions. ## 5. Delivery -- [ ] 5.1 Update `openspec/CHANGE_ORDER.md` status/dependency notes if implementation sequencing changed. +- [x] 5.1 Update `openspec/CHANGE_ORDER.md` status/dependency notes if implementation sequencing changed. - [ ] 5.2 Open a PR from `feature/requirements-02-module-commands` to `dev` with spec/test/code/docs evidence. diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml new file mode 100644 index 00000000..61cd64ac --- /dev/null +++ b/packages/specfact-requirements/module-package.yaml @@ -0,0 +1,22 @@ +name: nold-ai/specfact-requirements +version: 0.1.0 +commands: + - requirements +tier: official +publisher: + name: nold-ai + email: hello@noldai.com +bundle_dependencies: + - nold-ai/specfact-project +pip_dependencies: + - beartype + - icontract + - pydantic + - pyyaml + - typer +core_compatibility: '>=0.50.0,<1.0.0' +description: Official SpecFact requirements evidence runtime bundle package. +category: project +bundle_group_command: requirements +integrity: + checksum: sha256:5395e8553d98d4569dc7b690bc3326415105dfe78e86313b637d296cb6d08ab8 diff --git a/packages/specfact-requirements/src/specfact_requirements/__init__.py b/packages/specfact-requirements/src/specfact_requirements/__init__.py new file mode 100644 index 00000000..0a0f0fb7 --- /dev/null +++ b/packages/specfact-requirements/src/specfact_requirements/__init__.py @@ -0,0 +1,5 @@ +"""SpecFact requirements runtime module.""" + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/__init__.py b/packages/specfact-requirements/src/specfact_requirements/requirements/__init__.py new file mode 100644 index 00000000..91e8d6d9 --- /dev/null +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/__init__.py @@ -0,0 +1,6 @@ +"""Requirements command group.""" + +from specfact_requirements.requirements.commands import app + + +__all__ = ["app"] diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/app.py b/packages/specfact-requirements/src/specfact_requirements/requirements/app.py new file mode 100644 index 00000000..551a4b3d --- /dev/null +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/app.py @@ -0,0 +1,6 @@ +"""requirements command entrypoint.""" + +from specfact_requirements.requirements.commands import app + + +__all__ = ["app"] diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py new file mode 100644 index 00000000..313952a9 --- /dev/null +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py @@ -0,0 +1,138 @@ +"""Command handlers for requirement context evidence.""" + +from __future__ import annotations + +import json +from enum import StrEnum +from pathlib import Path +from typing import Annotated, Any, cast + +import typer +from beartype import beartype +from icontract import ensure, require +from specfact_cli.requirements.context import KNOWN_REQUIREMENT_CONTEXT_PROFILES, RequirementContextValidationProfile + +from specfact_requirements.requirements.runtime import ( + import_requirements_file_to_bundle, + inspect_requirements_bundle_coverage, + list_requirements_with_coverage, + validate_requirements_bundle, +) + + +class OutputFormat(StrEnum): + """Supported command output formats.""" + + JSON = "json" + TEXT = "text" + + +app = typer.Typer( + help="Import, validate, and inspect requirement context evidence.", + no_args_is_help=True, + context_settings={"help_option_names": ["-h", "--help", "--help-advanced", "-ha"]}, +) + + +def _format_supported(output_format: OutputFormat) -> bool: + return output_format in {OutputFormat.JSON, OutputFormat.TEXT} + + +def _profile_supported(profile: str) -> bool: + return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES + + +@beartype +@require(_format_supported, "output format must be supported") +@ensure(lambda result: result is None) +def _emit_payload(payload: dict[str, Any], output_format: OutputFormat) -> None: + if output_format == OutputFormat.JSON: + typer.echo(json.dumps(payload, indent=2, sort_keys=True)) + return + for key, value in payload.items(): + typer.echo(f"{key}: {value}") + + +@app.command("import", help="Import local requirement records into a project bundle.") +@beartype +@require(lambda from_file: from_file.is_file(), "from_file must exist") +@require(lambda bundle: bundle.is_dir(), "bundle must exist") +@require(_format_supported, "output format must be supported") +@ensure(lambda result: result is None) +def import_command( + from_file: Annotated[ + Path, + typer.Option( + "--from-file", exists=True, file_okay=True, dir_okay=False, readable=True, help="JSON/YAML records." + ), + ], + bundle: Annotated[ + Path, + typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True, writable=True), + ], + output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, +) -> None: + """Import local requirement records into a project bundle.""" + result = import_requirements_file_to_bundle(from_file, bundle) + _emit_payload( + { + "imported": len(result.requirements), + "diagnostics": [diagnostic.model_dump(mode="json") for diagnostic in result.diagnostics], + }, + output_format, + ) + + +@app.command("validate", help="Validate requirement context evidence usefulness.") +@beartype +@require(lambda bundle: bundle.is_dir(), "bundle must exist") +@require(_profile_supported, "profile must be a known requirement context profile") +@require(_format_supported, "output format must be supported") +@ensure(lambda result: result is None) +def validate_command( + bundle: Annotated[ + Path, + typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True), + ], + profile: Annotated[str, typer.Option("--profile", help="Validation profile.")] = "startup", + output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, +) -> None: + """Validate requirement context evidence usefulness.""" + report = validate_requirements_bundle(bundle, profile=cast(RequirementContextValidationProfile, profile)) + _emit_payload(report.model_dump(mode="json"), output_format) + if report.status == "failed": + raise typer.Exit(1) + + +@app.command("list", help="List normalized requirement inputs attached to a bundle.") +@beartype +@require(lambda bundle: bundle.is_dir(), "bundle must exist") +@require(_format_supported, "output format must be supported") +@ensure(lambda result: result is None) +def list_command( + bundle: Annotated[ + Path, + typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True), + ], + show_coverage: Annotated[bool, typer.Option("--show-coverage", help="Include coverage summary.")] = False, + output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, +) -> None: + """List normalized requirement inputs attached to a bundle.""" + _emit_payload(list_requirements_with_coverage(bundle, show_coverage=show_coverage), output_format) + + +@app.command("coverage", help="Inspect requirement context coverage.") +@beartype +@require(lambda bundle: bundle.is_dir(), "bundle must exist") +@require(_format_supported, "output format must be supported") +@ensure(lambda result: result is None) +def coverage_command( + bundle: Annotated[ + Path, + typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True), + ], + output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, +) -> None: + """Inspect requirement context coverage.""" + coverage = inspect_requirements_bundle_coverage(bundle) + _emit_payload(coverage.model_dump(mode="json"), output_format) diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py new file mode 100644 index 00000000..5a06a7b3 --- /dev/null +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py @@ -0,0 +1,165 @@ +"""Runtime helpers for requirements context commands.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any, cast + +import yaml +from beartype import beartype +from icontract import ensure, require +from specfact_cli.models.project import ProjectBundle +from specfact_cli.models.requirements import ( + RequirementInput, + load_requirements_input_extension, + requirements_input_extension_payload, +) +from specfact_cli.models.validation import ValidationReport +from specfact_cli.requirements.context import ( + KNOWN_REQUIREMENT_CONTEXT_PROFILES, + RequirementContextCoverageSummary, + RequirementContextImportResult, + RequirementContextValidationProfile, + attach_requirements_to_bundle, + inspect_requirement_context_coverage, + load_requirements_from_bundle, + normalize_requirement_records, + validate_requirement_context, +) +from specfact_cli.utils.bundle_loader import load_project_bundle, save_project_bundle + + +_REQUIREMENTS_INPUTS_FILE = "requirements.inputs.yaml" + + +def _records_are_supported(result: Sequence[RequirementInput | Mapping[str, Any]]) -> bool: + return all(isinstance(record, RequirementInput | Mapping) for record in result) + + +def _result_has_expected_keys(result: dict[str, Any]) -> bool: + return "requirements" in result + + +def _profile_is_supported(profile: str) -> bool: + return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES + + +def _requirements_sidecar_path(bundle_dir: Path) -> Path: + return bundle_dir / _REQUIREMENTS_INPUTS_FILE + + +def _load_bundle_with_requirements(bundle_dir: Path) -> ProjectBundle: + bundle = load_project_bundle(bundle_dir) + sidecar = _requirements_sidecar_path(bundle_dir) + if not sidecar.exists(): + return bundle + payload = yaml.safe_load(sidecar.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + msg = f"{sidecar} must contain a requirements.inputs mapping payload" + raise ValueError(msg) + records = load_requirements_input_extension(cast(dict[str, Any], payload)) + attach_requirements_to_bundle(bundle, records) + return bundle + + +def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[RequirementInput]) -> None: + payload = requirements_input_extension_payload(list(records)) + _requirements_sidecar_path(bundle_dir).write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + + +@beartype +@require(lambda source_file: source_file.is_file(), "source_file must exist") +@ensure(_records_are_supported, "all loaded records must be RequirementInput or mapping values") +def load_requirement_records(source_file: Path) -> list[RequirementInput | Mapping[str, Any]]: + """Load requirement records from a JSON or YAML source file.""" + loaded = yaml.safe_load(source_file.read_text(encoding="utf-8")) + if loaded is None: + return [] + if isinstance(loaded, list): + return cast(list[RequirementInput | Mapping[str, Any]], loaded) + if isinstance(loaded, dict): + maybe_records = loaded.get("requirements") + if isinstance(maybe_records, list): + return cast(list[RequirementInput | Mapping[str, Any]], maybe_records) + return [cast(Mapping[str, Any], loaded)] + msg = "requirements source must be a mapping, a list, or a mapping with a requirements list" + raise ValueError(msg) + + +@beartype +@require(lambda existing: all(isinstance(record, RequirementInput) for record in existing)) +@require(lambda imported: all(isinstance(record, RequirementInput) for record in imported)) +@ensure(lambda result: all(isinstance(record, RequirementInput) for record in result)) +def merge_requirement_inputs( + existing: Sequence[RequirementInput], + imported: Sequence[RequirementInput], +) -> list[RequirementInput]: + """Merge imported requirements by stable ID while preserving existing order.""" + by_id = {record.requirement_id: record for record in existing} + order = [record.requirement_id for record in existing] + for record in imported: + if record.requirement_id not in by_id: + order.append(record.requirement_id) + by_id[record.requirement_id] = record + return [by_id[requirement_id] for requirement_id in order] + + +@beartype +@require(lambda source_file: source_file.is_file(), "source_file must exist") +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@ensure(lambda result: isinstance(result, RequirementContextImportResult)) +def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> RequirementContextImportResult: + """Normalize requirement records from a file and attach valid records to a ProjectBundle.""" + records = load_requirement_records(source_file) + result = normalize_requirement_records(records, source_locator=source_file.as_posix()) + bundle = _load_bundle_with_requirements(bundle_dir) + merged = merge_requirement_inputs(load_requirements_from_bundle(bundle), result.requirements) + attach_requirements_to_bundle(bundle, merged) + save_project_bundle(bundle, bundle_dir, atomic=True) + _write_requirements_sidecar(bundle_dir, merged) + return result + + +@beartype +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@require(_profile_is_supported, "profile must be a known requirement context profile") +@ensure(lambda result: isinstance(result, ValidationReport)) +def validate_requirements_bundle( + bundle_dir: Path, *, profile: RequirementContextValidationProfile = "startup" +) -> ValidationReport: + """Validate requirement context evidence usefulness for a bundle.""" + bundle = _load_bundle_with_requirements(bundle_dir) + return validate_requirement_context(bundle, profile=profile) + + +@beartype +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@ensure(lambda result: isinstance(result, RequirementContextCoverageSummary)) +def inspect_requirements_bundle_coverage(bundle_dir: Path) -> RequirementContextCoverageSummary: + """Inspect coverage for normalized requirement inputs attached to a bundle.""" + bundle = _load_bundle_with_requirements(bundle_dir) + return inspect_requirement_context_coverage(load_requirements_from_bundle(bundle)) + + +@beartype +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@ensure(_result_has_expected_keys) +def list_requirements_with_coverage(bundle_dir: Path, *, show_coverage: bool = False) -> dict[str, Any]: + """Return serializable requirement rows and optional coverage summary.""" + bundle = _load_bundle_with_requirements(bundle_dir) + requirements = load_requirements_from_bundle(bundle) + payload: dict[str, Any] = { + "requirements": [ + { + "requirement_id": requirement.requirement_id, + "title": requirement.title, + "source_count": len(requirement.sources), + "evidence_link_count": len(requirement.evidence_links), + } + for requirement in requirements + ] + } + if show_coverage: + payload["coverage"] = inspect_requirement_context_coverage(requirements).model_dump(mode="json") + return payload diff --git a/pyproject.toml b/pyproject.toml index 4b87be4b..67514128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ extraPaths = [ "packages/specfact-code-review/src", "packages/specfact-spec/src", "packages/specfact-govern/src", + "packages/specfact-requirements/src", ] reportMissingImports = true reportMissingTypeStubs = false @@ -119,6 +120,7 @@ executionEnvironments = [ { root = "packages/specfact-code-review/src", reportMissingImports = false, reportAttributeAccessIssue = false }, { root = "packages/specfact-spec/src", reportMissingImports = false, reportAttributeAccessIssue = false }, { root = "packages/specfact-govern/src", reportMissingImports = false, reportAttributeAccessIssue = false }, + { root = "packages/specfact-requirements/src", reportMissingImports = false, reportAttributeAccessIssue = false }, ] reportAttributeAccessIssue = false @@ -133,6 +135,7 @@ pythonpath = [ "packages/specfact-code-review/src", "packages/specfact-spec/src", "packages/specfact-govern/src", + "packages/specfact-requirements/src", ] testpaths = ["tests"] python_files = ["test_*.py"] @@ -282,6 +285,7 @@ known-first-party = [ "specfact_code_review", "specfact_spec", "specfact_govern", + "specfact_requirements", "specfact_cli_modules", ] diff --git a/scripts/check-bundle-imports.py b/scripts/check-bundle-imports.py index 421d3ba1..a5078c56 100755 --- a/scripts/check-bundle-imports.py +++ b/scripts/check-bundle-imports.py @@ -9,6 +9,9 @@ from dataclasses import dataclass from pathlib import Path +from beartype import beartype +from icontract import ensure + ROOT = Path(__file__).resolve().parent.parent @@ -18,6 +21,7 @@ "specfact_codebase": ROOT / "packages/specfact-codebase/src/specfact_codebase", "specfact_spec": ROOT / "packages/specfact-spec/src/specfact_spec", "specfact_govern": ROOT / "packages/specfact-govern/src/specfact_govern", + "specfact_requirements": ROOT / "packages/specfact-requirements/src/specfact_requirements", } ALLOWED_SPECFACT_CLI_EXACT: set[str] = { @@ -32,6 +36,7 @@ "specfact_cli.contracts.module_interface", "specfact_cli.integrations.specmatic", "specfact_cli.models", + "specfact_cli.requirements", "specfact_cli.modes", "specfact_cli.modules", "specfact_cli.registry.registry", @@ -60,6 +65,7 @@ "specfact_codebase": {"specfact_project"}, "specfact_spec": {"specfact_project"}, "specfact_govern": {"specfact_project"}, + "specfact_requirements": set(), } @@ -111,6 +117,17 @@ def _visit(node: ast.AST) -> None: return imports +def _cross_bundle_violation(bundle_name: str, module_name: str) -> str | None: + for other_bundle in PACKAGE_DIRS: + if other_bundle == bundle_name: + continue + if module_name == other_bundle or module_name.startswith(other_bundle + "."): + if other_bundle not in ALLOWED_CROSS_BUNDLE_IMPORTS.get(bundle_name, set()): + return other_bundle + return None + return None + + def _scan_python_file(bundle_name: str, file_path: Path) -> list[Violation]: violations: list[Violation] = [] try: @@ -140,24 +157,22 @@ def _scan_python_file(bundle_name: str, file_path: Path) -> list[Violation]: ) continue - for other_bundle in PACKAGE_DIRS: - if other_bundle == bundle_name: - continue - if module_name == other_bundle or module_name.startswith(other_bundle + "."): - if other_bundle not in ALLOWED_CROSS_BUNDLE_IMPORTS.get(bundle_name, set()): - violations.append( - Violation( - file_path=file_path, - line=line, - import_name=module_name, - message=f"Forbidden cross-bundle import: {bundle_name} -> {other_bundle}", - ) - ) - break + other_bundle = _cross_bundle_violation(bundle_name, module_name) + if other_bundle is not None: + violations.append( + Violation( + file_path=file_path, + line=line, + import_name=module_name, + message=f"Forbidden cross-bundle import: {bundle_name} -> {other_bundle}", + ) + ) return violations +@beartype +@ensure(lambda result: result in {0, 1}) def main() -> int: violations: list[Violation] = [] @@ -168,13 +183,13 @@ def main() -> int: violations.extend(_scan_python_file(bundle_name, file_path)) if not violations: - print("Import boundary check passed.") + sys.stdout.write("Import boundary check passed.\n") return 0 - print("Import boundary violations found:") + sys.stdout.write("Import boundary violations found:\n") for violation in sorted(violations, key=lambda v: (str(v.file_path), v.line, v.import_name)): rel_path = violation.file_path.relative_to(ROOT) - print(f"- {rel_path}:{violation.line} [{violation.import_name}] {violation.message}") + sys.stdout.write(f"- {rel_path}:{violation.line} [{violation.import_name}] {violation.message}\n") return 1 diff --git a/scripts/check-command-contract.py b/scripts/check-command-contract.py index f31c0310..9d4faa22 100644 --- a/scripts/check-command-contract.py +++ b/scripts/check-command-contract.py @@ -13,6 +13,8 @@ from typing import Any, cast import typer +from beartype import beartype +from icontract import ensure from typer.testing import CliRunner @@ -24,6 +26,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 = ( @@ -95,9 +98,7 @@ def _select_app(apps: dict[tuple[str, ...], object], command_parts: list[str]) - continue if best_prefix is None or len(prefix) > len(best_prefix): best_prefix = prefix - if best_prefix is None: - return None - return apps[best_prefix], command_parts[len(best_prefix) :] + return None if best_prefix is None else (apps[best_prefix], command_parts[len(best_prefix) :]) def _invoke( @@ -131,6 +132,20 @@ def _has_required_argument(record: dict[str, Any]) -> bool: return any(isinstance(argument, dict) and argument.get("required") for argument in arguments) +def _usage_lines(output: str) -> list[str]: + usage_lines: list[str] = [] + capture_usage = False + for line in output.splitlines(): + if "Usage:" in line: + capture_usage = True + if not capture_usage: + continue + if not line.strip(): + break + usage_lines.append(line.lower()) + return usage_lines + + def _check_help(runner: CliRunner, apps: dict[tuple[str, ...], object], record: dict[str, Any]) -> list[str]: command_parts = _command_parts(record) exit_code, output = _invoke(runner, apps, command_parts, ["--help"]) @@ -142,15 +157,7 @@ def _check_help(runner: CliRunner, apps: dict[tuple[str, ...], object], record: selected_args = selected[1] if selected is not None else [] if not _is_group(record) and selected_args: command_parts = _command_parts(record) - usage_lines = [] - capture_usage = False - for line in output.splitlines(): - if "Usage:" in line: - capture_usage = True - if capture_usage: - if not line.strip(): - break - usage_lines.append(line.lower()) + usage_lines = _usage_lines(output) if command_parts and command_parts[-1].lower() not in " ".join(usage_lines): return [f"{record.get('command')}: --help rendered parent usage instead of leaf usage\n{output}"] return [] @@ -204,6 +211,8 @@ def _check_missing_required_argument( return failures +@beartype +@ensure(lambda result: result in {0, 1}) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--limit", type=int, default=0, help="Validate only the first N generated commands") @@ -222,10 +231,11 @@ def main(argv: list[str] | None = None) -> int: failures.extend(_check_missing_required_argument(runner, apps, record)) if failures: - print("Generated module command contract validation failed:") - print("\n\n".join(failures)) + sys.stdout.write("Generated module command contract validation failed:\n") + sys.stdout.write("\n\n".join(failures)) + sys.stdout.write("\n") return 1 - print(f"check-command-contract: OK ({len(records)} generated module command path(s) validated)") + sys.stdout.write(f"check-command-contract: OK ({len(records)} generated module command path(s) validated)\n") return 0 diff --git a/scripts/check-docs-commands.py b/scripts/check-docs-commands.py index 73805c72..41140741 100755 --- a/scripts/check-docs-commands.py +++ b/scripts/check-docs-commands.py @@ -81,6 +81,7 @@ class CommandExample(NamedTuple): ("specfact_govern.govern.commands", "app", ("specfact", "govern")), ("specfact_govern.enforce.commands", "app", ("specfact", "govern", "enforce")), ("specfact_project.project.commands", "app", ("specfact", "project")), + ("specfact_requirements.requirements.commands", "app", ("specfact", "requirements")), ("specfact_spec.contract.commands", "app", ("specfact", "spec", "contract")), ("specfact_spec.spec.commands", "app", ("specfact", "spec")), ("specfact_spec.sdd.commands", "app", ("specfact", "spec")), diff --git a/scripts/check-prompt-commands.py b/scripts/check-prompt-commands.py index e4a7d43f..e4e3a6e6 100644 --- a/scripts/check-prompt-commands.py +++ b/scripts/check-prompt-commands.py @@ -40,6 +40,7 @@ ("specfact_govern.govern.commands", "app", ("specfact", "govern")), ("specfact_govern.enforce.commands", "app", ("specfact", "govern", "enforce")), ("specfact_project.project.commands", "app", ("specfact", "project")), + ("specfact_requirements.requirements.commands", "app", ("specfact", "requirements")), ("specfact_spec.contract.commands", "app", ("specfact", "spec", "contract")), ("specfact_spec.spec.commands", "app", ("specfact", "spec")), ("specfact_spec.sdd.commands", "app", ("specfact", "spec", "sdd")), diff --git a/scripts/generate-command-overview.py b/scripts/generate-command-overview.py index f1e5059a..30df6718 100644 --- a/scripts/generate-command-overview.py +++ b/scripts/generate-command-overview.py @@ -14,6 +14,8 @@ from typing import Any, cast import click +from beartype import beartype +from icontract import ensure from typer.main import get_command @@ -28,6 +30,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"), ) @@ -152,6 +160,8 @@ def _walk(command: click.Command, path: tuple[str, ...], source: str, module_id: return records +@beartype +@ensure(lambda result: all("command" in record for record in result)) def build_records() -> list[dict[str, Any]]: _ensure_package_paths() records: list[dict[str, Any]] = [] @@ -200,7 +210,10 @@ def _render_llms(markdown: str) -> str: [ "# SpecFact Module Commands", "", - "Use this generated overview as the current module command contract before following older docs or prompts.", + ( + "Use this generated overview as the current module command contract " + "before following older docs or prompts." + ), "", markdown, ] @@ -223,7 +236,7 @@ def _check(outputs: dict[Path, str]) -> int: actual = path.read_text(encoding="utf-8") if path.exists() else "" if actual != expected: failures.append(path) - print( + sys.stdout.write( "\n".join( difflib.unified_diff( actual.splitlines(), @@ -233,13 +246,18 @@ def _check(outputs: dict[Path, str]) -> int: lineterm="", ) ) + + "\n" ) if failures: - print("Module command overview artifacts are stale. Run: python scripts/generate-command-overview.py --write") + sys.stdout.write( + "Module command overview artifacts are stale. Run: python scripts/generate-command-overview.py --write\n" + ) return 1 return 0 +@beartype +@ensure(lambda result: result in {0, 1}) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--write", action="store_true", help="Write generated artifacts") diff --git a/tests/conftest.py b/tests/conftest.py index f3a13213..f3a9f4f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ "specfact_codebase", "specfact_spec", "specfact_govern", + "specfact_requirements", ) diff --git a/tests/integration/specfact_requirements/test_command_apps.py b/tests/integration/specfact_requirements/test_command_apps.py new file mode 100644 index 00000000..062ba6fe --- /dev/null +++ b/tests/integration/specfact_requirements/test_command_apps.py @@ -0,0 +1,92 @@ +"""Integration tests for the requirements command app.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from specfact_cli.common.bundle_factory import create_empty_project_bundle +from specfact_cli.utils.bundle_loader import save_project_bundle +from typer.testing import CliRunner + +from specfact_requirements.requirements.commands import app + + +runner = CliRunner() + + +def _bundle_dir(tmp_path: Path) -> Path: + bundle_dir = tmp_path / "bundle" + bundle = create_empty_project_bundle("bundle") + save_project_bundle(bundle, bundle_dir, atomic=False) + return bundle_dir + + +def _source_file(tmp_path: Path) -> Path: + source = tmp_path / "requirements.json" + source.write_text( + json.dumps( + [ + { + "schema_version": "1", + "requirement_id": "REQ-CLI", + "title": "CLI imports requirement context", + "sources": [ + { + "source_type": "issue", + "locator": "https://github.com/nold-ai/specfact-cli-modules/issues/165", + } + ], + } + ] + ), + encoding="utf-8", + ) + return source + + +@pytest.mark.integration +def test_command_module_exposes_typer_app() -> None: + assert app is not None + assert hasattr(app, "registered_commands") + + +@pytest.mark.integration +def test_requirements_import_list_and_validate_commands_emit_json(tmp_path: Path) -> None: + bundle_dir = _bundle_dir(tmp_path) + source = _source_file(tmp_path) + + import_result = runner.invoke( + app, + ["import", "--from-file", str(source), "--bundle", str(bundle_dir), "--format", "json"], + ) + + assert import_result.exit_code == 0, import_result.output + assert json.loads(import_result.output)["imported"] == 1 + + list_result = runner.invoke(app, ["list", "--bundle", str(bundle_dir), "--show-coverage", "--format", "json"]) + + assert list_result.exit_code == 0, list_result.output + list_payload = json.loads(list_result.output) + assert list_payload["requirements"][0]["requirement_id"] == "REQ-CLI" + assert list_payload["coverage"]["missing_evidence_requirement_ids"] == ["REQ-CLI"] + + validate_result = runner.invoke( + app, + ["validate", "--bundle", str(bundle_dir), "--profile", "enterprise", "--format", "json"], + ) + + assert validate_result.exit_code == 1 + assert json.loads(validate_result.output)["status"] == "failed" + + +@pytest.mark.integration +def test_requirements_help_exposes_no_author_command() -> None: + result = runner.invoke(app, ["--help"]) + + assert result.exit_code == 0 + assert "import" in result.output + assert "validate" in result.output + assert "coverage" in result.output + assert "author" not in result.output diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index a70e430c..aa3ce5c5 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -55,6 +55,26 @@ "--help", ], "specfact backlog daily github --state open --limit 20": ["backlog", "daily", "--help"], + "specfact requirements import --from-file requirements.json --bundle .specfact/projects/shop --format json": [ + "requirements", + "import", + "--help", + ], + "specfact requirements list --bundle .specfact/projects/shop --show-coverage --format json": [ + "requirements", + "list", + "--help", + ], + "specfact requirements validate --bundle .specfact/projects/shop --profile enterprise --format json": [ + "requirements", + "validate", + "--help", + ], + "specfact requirements coverage --bundle .specfact/projects/shop --format json": [ + "requirements", + "coverage", + "--help", + ], "specfact spec sdd list --repo .": ["spec", "sdd", "list", "--help"], } @@ -109,6 +129,7 @@ def _route_backlog(t: list[str]) -> tuple[Any, list[str]] | None: "plan": "specfact_project.plan.commands", "sync": "specfact_project.sync.commands", "migrate": "specfact_project.migrate.commands", + "requirements": "specfact_requirements.requirements.commands", } diff --git a/tests/unit/specfact_requirements/test_requirements_runtime.py b/tests/unit/specfact_requirements/test_requirements_runtime.py new file mode 100644 index 00000000..3cd8a7b7 --- /dev/null +++ b/tests/unit/specfact_requirements/test_requirements_runtime.py @@ -0,0 +1,89 @@ +"""Tests for the requirements runtime module.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from specfact_cli.common.bundle_factory import create_empty_project_bundle +from specfact_cli.utils.bundle_loader import save_project_bundle + +from specfact_requirements.requirements.runtime import ( + import_requirements_file_to_bundle, + list_requirements_with_coverage, + validate_requirements_bundle, +) + + +def _requirement_record(requirement_id: str = "REQ-165", *, with_evidence: bool = False) -> dict[str, object]: + record: dict[str, object] = { + "schema_version": "1", + "requirement_id": requirement_id, + "title": "Requirement context is imported as validation evidence", + "sources": [ + { + "source_type": "issue", + "locator": "https://github.com/nold-ai/specfact-cli-modules/issues/165", + "title": "Requirements Input Runtime Commands Follow-Up", + } + ], + } + if with_evidence: + record["evidence_links"] = [{"link_type": "test", "target": "tests/unit/specfact_requirements"}] + return record + + +def _bundle_dir(tmp_path: Path) -> Path: + bundle_dir = tmp_path / "bundle" + bundle = create_empty_project_bundle("bundle") + save_project_bundle(bundle, bundle_dir, atomic=False) + return bundle_dir + + +def test_import_requirements_file_to_bundle_preserves_valid_records_and_diagnostics(tmp_path: Path) -> None: + source = tmp_path / "requirements.json" + source.write_text( + json.dumps( + { + "requirements": [ + _requirement_record(), + {"requirement_id": "REQ-BROKEN", "schema_version": "1", "title": "Missing source refs"}, + ] + } + ), + encoding="utf-8", + ) + bundle_dir = _bundle_dir(tmp_path) + + result = import_requirements_file_to_bundle(source, bundle_dir) + + assert [record.requirement_id for record in result.requirements] == ["REQ-165"] + assert [diagnostic.requirement_id for diagnostic in result.diagnostics] == ["REQ-BROKEN"] + listing = list_requirements_with_coverage(bundle_dir) + assert [record["requirement_id"] for record in listing["requirements"]] == ["REQ-165"] + + +def test_validate_requirements_bundle_uses_profile_aware_core_validation(tmp_path: Path) -> None: + source = tmp_path / "requirements.json" + source.write_text(json.dumps([_requirement_record()]), encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + import_requirements_file_to_bundle(source, bundle_dir) + + report = validate_requirements_bundle(bundle_dir, profile="enterprise") + + assert report.status == "failed" + assert report.violations[0]["location"] == "requirements.inputs[REQ-165].evidence_links" + + +def test_list_requirements_with_coverage_is_machine_readable(tmp_path: Path) -> None: + source = tmp_path / "requirements.json" + source.write_text(json.dumps([_requirement_record("REQ-COVERED", with_evidence=True)]), encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + import_requirements_file_to_bundle(source, bundle_dir) + + listing = list_requirements_with_coverage(bundle_dir, show_coverage=True) + + assert listing["requirements"][0]["requirement_id"] == "REQ-COVERED" + assert listing["requirements"][0]["title"] == "Requirement context is imported as validation evidence" + assert listing["coverage"]["total_requirements"] == 1 + assert listing["coverage"]["with_test_links"] == 1 From 629131802b7cd13a699c1398d90f76c17dfe5f95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:48:41 +0000 Subject: [PATCH 02/15] chore(modules): ci sign changed modules --- packages/specfact-requirements/module-package.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 61cd64ac..6cb261e9 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -20,3 +20,4 @@ category: project bundle_group_command: requirements integrity: checksum: sha256:5395e8553d98d4569dc7b690bc3326415105dfe78e86313b637d296cb6d08ab8 + signature: wNdVSL7OrA2s4Br/gYQ5bRO8H2TjV2xnqoj+YeWyQkf3rbJSw7rluQja9/r4PpV/9qXK7EGn9bvNQnCATvXzBw== From 15918f9a538d295cf477099675f050e88d4b2479 Mon Sep 17 00:00:00 2001 From: Dom <39115308+djm81@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:50:25 +0200 Subject: [PATCH 03/15] docs: mark requirements module PR task complete --- openspec/changes/requirements-02-module-commands/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/requirements-02-module-commands/tasks.md b/openspec/changes/requirements-02-module-commands/tasks.md index ca1ed0d5..ab43561c 100644 --- a/openspec/changes/requirements-02-module-commands/tasks.md +++ b/openspec/changes/requirements-02-module-commands/tasks.md @@ -33,4 +33,4 @@ ## 5. Delivery - [x] 5.1 Update `openspec/CHANGE_ORDER.md` status/dependency notes if implementation sequencing changed. -- [ ] 5.2 Open a PR from `feature/requirements-02-module-commands` to `dev` with spec/test/code/docs evidence. +- [x] 5.2 Open a PR from `feature/requirements-02-module-commands` to `dev` with spec/test/code/docs evidence: https://github.com/nold-ai/specfact-cli-modules/pull/326 From 43d40dd844b0bbac07d1679e24d0d95c95863b8a Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 8 Jul 2026 22:06:35 +0200 Subject: [PATCH 04/15] fix: lazy-load requirements core helpers --- .../specfact-requirements/module-package.yaml | 5 +- .../requirements/commands.py | 6 +- .../requirements/runtime.py | 113 +++++++++++------- .../test_command_apps.py | 29 +++++ 4 files changed, 102 insertions(+), 51 deletions(-) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 6cb261e9..6da3ab4c 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-requirements -version: 0.1.0 +version: 0.1.1 commands: - requirements tier: official @@ -19,5 +19,4 @@ description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:5395e8553d98d4569dc7b690bc3326415105dfe78e86313b637d296cb6d08ab8 - signature: wNdVSL7OrA2s4Br/gYQ5bRO8H2TjV2xnqoj+YeWyQkf3rbJSw7rluQja9/r4PpV/9qXK7EGn9bvNQnCATvXzBw== + checksum: sha256:8911ec1e0dad47257fcc803be46be868898d58fa6c3b6925b37dd8849dfd8f3a diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py index 313952a9..e0c9ed1c 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py @@ -5,14 +5,14 @@ import json from enum import StrEnum from pathlib import Path -from typing import Annotated, Any, cast +from typing import Annotated, Any import typer from beartype import beartype from icontract import ensure, require -from specfact_cli.requirements.context import KNOWN_REQUIREMENT_CONTEXT_PROFILES, RequirementContextValidationProfile from specfact_requirements.requirements.runtime import ( + KNOWN_REQUIREMENT_CONTEXT_PROFILES, import_requirements_file_to_bundle, inspect_requirements_bundle_coverage, list_requirements_with_coverage, @@ -98,7 +98,7 @@ def validate_command( output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, ) -> None: """Validate requirement context evidence usefulness.""" - report = validate_requirements_bundle(bundle, profile=cast(RequirementContextValidationProfile, profile)) + report = validate_requirements_bundle(bundle, profile=profile) _emit_payload(report.model_dump(mode="json"), output_format) if report.status == "failed": raise typer.Exit(1) diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py index 5a06a7b3..fbf684db 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence +from importlib import import_module from pathlib import Path from typing import Any, cast @@ -10,37 +11,54 @@ from beartype import beartype from icontract import ensure, require from specfact_cli.models.project import ProjectBundle -from specfact_cli.models.requirements import ( - RequirementInput, - load_requirements_input_extension, - requirements_input_extension_payload, -) from specfact_cli.models.validation import ValidationReport -from specfact_cli.requirements.context import ( - KNOWN_REQUIREMENT_CONTEXT_PROFILES, - RequirementContextCoverageSummary, - RequirementContextImportResult, - RequirementContextValidationProfile, - attach_requirements_to_bundle, - inspect_requirement_context_coverage, - load_requirements_from_bundle, - normalize_requirement_records, - validate_requirement_context, -) from specfact_cli.utils.bundle_loader import load_project_bundle, save_project_bundle _REQUIREMENTS_INPUTS_FILE = "requirements.inputs.yaml" +_REQUIREMENTS_MODELS_MODULE = "specfact_cli.models.requirements" +_REQUIREMENTS_CONTEXT_MODULE = "specfact_cli.requirements.context" +KNOWN_REQUIREMENT_CONTEXT_PROFILES: frozenset[str] = frozenset( + { + "solo", + "startup", + "team", + "enterprise", + "strict", + "solo_developer", + "api_first_team", + "enterprise_full_stack", + } +) + +class RequirementsCoreUnavailableError(RuntimeError): + """Raised when the paired core requirements helpers are unavailable.""" -def _records_are_supported(result: Sequence[RequirementInput | Mapping[str, Any]]) -> bool: - return all(isinstance(record, RequirementInput | Mapping) for record in result) + +def _load_requirements_module(module_name: str, purpose: str) -> Any: + try: + return import_module(module_name) + except ImportError as exc: + msg = ( + f"specfact-requirements requires the paired specfact-cli {purpose} " + "from core change requirements-02-module-commands" + ) + raise RequirementsCoreUnavailableError(msg) from exc + + +def _records_are_supported(result: Sequence[Mapping[str, Any]]) -> bool: + return all(isinstance(record, Mapping) for record in result) def _result_has_expected_keys(result: dict[str, Any]) -> bool: return "requirements" in result +def _has_attributes(result: Any, *attributes: str) -> bool: + return all(hasattr(result, attribute) for attribute in attributes) + + def _profile_is_supported(profile: str) -> bool: return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES @@ -58,43 +76,46 @@ def _load_bundle_with_requirements(bundle_dir: Path) -> ProjectBundle: if not isinstance(payload, dict): msg = f"{sidecar} must contain a requirements.inputs mapping payload" raise ValueError(msg) - records = load_requirements_input_extension(cast(dict[str, Any], payload)) - attach_requirements_to_bundle(bundle, records) + model_helpers = _load_requirements_module(_REQUIREMENTS_MODELS_MODULE, "requirements models") + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + records = model_helpers.load_requirements_input_extension(cast(dict[str, Any], payload)) + context_helpers.attach_requirements_to_bundle(bundle, records) return bundle -def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[RequirementInput]) -> None: - payload = requirements_input_extension_payload(list(records)) +def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[Any]) -> None: + model_helpers = _load_requirements_module(_REQUIREMENTS_MODELS_MODULE, "requirements models") + payload = model_helpers.requirements_input_extension_payload(list(records)) _requirements_sidecar_path(bundle_dir).write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") @beartype @require(lambda source_file: source_file.is_file(), "source_file must exist") @ensure(_records_are_supported, "all loaded records must be RequirementInput or mapping values") -def load_requirement_records(source_file: Path) -> list[RequirementInput | Mapping[str, Any]]: +def load_requirement_records(source_file: Path) -> list[Mapping[str, Any]]: """Load requirement records from a JSON or YAML source file.""" loaded = yaml.safe_load(source_file.read_text(encoding="utf-8")) if loaded is None: return [] if isinstance(loaded, list): - return cast(list[RequirementInput | Mapping[str, Any]], loaded) + return cast(list[Mapping[str, Any]], loaded) if isinstance(loaded, dict): maybe_records = loaded.get("requirements") if isinstance(maybe_records, list): - return cast(list[RequirementInput | Mapping[str, Any]], maybe_records) + return cast(list[Mapping[str, Any]], maybe_records) return [cast(Mapping[str, Any], loaded)] msg = "requirements source must be a mapping, a list, or a mapping with a requirements list" raise ValueError(msg) @beartype -@require(lambda existing: all(isinstance(record, RequirementInput) for record in existing)) -@require(lambda imported: all(isinstance(record, RequirementInput) for record in imported)) -@ensure(lambda result: all(isinstance(record, RequirementInput) for record in result)) +@require(lambda existing: all(hasattr(record, "requirement_id") for record in existing)) +@require(lambda imported: all(hasattr(record, "requirement_id") for record in imported)) +@ensure(lambda result: all(hasattr(record, "requirement_id") for record in result)) def merge_requirement_inputs( - existing: Sequence[RequirementInput], - imported: Sequence[RequirementInput], -) -> list[RequirementInput]: + existing: Sequence[Any], + imported: Sequence[Any], +) -> list[Any]: """Merge imported requirements by stable ID while preserving existing order.""" by_id = {record.requirement_id: record for record in existing} order = [record.requirement_id for record in existing] @@ -108,14 +129,15 @@ def merge_requirement_inputs( @beartype @require(lambda source_file: source_file.is_file(), "source_file must exist") @require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") -@ensure(lambda result: isinstance(result, RequirementContextImportResult)) -def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> RequirementContextImportResult: +@ensure(lambda result: _has_attributes(result, "requirements", "diagnostics")) +def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> Any: """Normalize requirement records from a file and attach valid records to a ProjectBundle.""" records = load_requirement_records(source_file) - result = normalize_requirement_records(records, source_locator=source_file.as_posix()) + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + result = context_helpers.normalize_requirement_records(records, source_locator=source_file.as_posix()) bundle = _load_bundle_with_requirements(bundle_dir) - merged = merge_requirement_inputs(load_requirements_from_bundle(bundle), result.requirements) - attach_requirements_to_bundle(bundle, merged) + merged = merge_requirement_inputs(context_helpers.load_requirements_from_bundle(bundle), result.requirements) + context_helpers.attach_requirements_to_bundle(bundle, merged) save_project_bundle(bundle, bundle_dir, atomic=True) _write_requirements_sidecar(bundle_dir, merged) return result @@ -125,21 +147,21 @@ def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> R @require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") @require(_profile_is_supported, "profile must be a known requirement context profile") @ensure(lambda result: isinstance(result, ValidationReport)) -def validate_requirements_bundle( - bundle_dir: Path, *, profile: RequirementContextValidationProfile = "startup" -) -> ValidationReport: +def validate_requirements_bundle(bundle_dir: Path, *, profile: str = "startup") -> ValidationReport: """Validate requirement context evidence usefulness for a bundle.""" bundle = _load_bundle_with_requirements(bundle_dir) - return validate_requirement_context(bundle, profile=profile) + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + return context_helpers.validate_requirement_context(bundle, profile=profile) @beartype @require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") -@ensure(lambda result: isinstance(result, RequirementContextCoverageSummary)) -def inspect_requirements_bundle_coverage(bundle_dir: Path) -> RequirementContextCoverageSummary: +@ensure(lambda result: _has_attributes(result, "total_requirements", "with_test_links")) +def inspect_requirements_bundle_coverage(bundle_dir: Path) -> Any: """Inspect coverage for normalized requirement inputs attached to a bundle.""" bundle = _load_bundle_with_requirements(bundle_dir) - return inspect_requirement_context_coverage(load_requirements_from_bundle(bundle)) + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + return context_helpers.inspect_requirement_context_coverage(context_helpers.load_requirements_from_bundle(bundle)) @beartype @@ -148,7 +170,8 @@ def inspect_requirements_bundle_coverage(bundle_dir: Path) -> RequirementContext def list_requirements_with_coverage(bundle_dir: Path, *, show_coverage: bool = False) -> dict[str, Any]: """Return serializable requirement rows and optional coverage summary.""" bundle = _load_bundle_with_requirements(bundle_dir) - requirements = load_requirements_from_bundle(bundle) + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + requirements = context_helpers.load_requirements_from_bundle(bundle) payload: dict[str, Any] = { "requirements": [ { @@ -161,5 +184,5 @@ def list_requirements_with_coverage(bundle_dir: Path, *, show_coverage: bool = F ] } if show_coverage: - payload["coverage"] = inspect_requirement_context_coverage(requirements).model_dump(mode="json") + payload["coverage"] = context_helpers.inspect_requirement_context_coverage(requirements).model_dump(mode="json") return payload diff --git a/tests/integration/specfact_requirements/test_command_apps.py b/tests/integration/specfact_requirements/test_command_apps.py index 062ba6fe..a0b4afc4 100644 --- a/tests/integration/specfact_requirements/test_command_apps.py +++ b/tests/integration/specfact_requirements/test_command_apps.py @@ -3,6 +3,9 @@ from __future__ import annotations import json +import os +import subprocess +import sys from pathlib import Path import pytest @@ -52,6 +55,32 @@ def test_command_module_exposes_typer_app() -> None: assert hasattr(app, "registered_commands") +@pytest.mark.integration +def test_command_module_import_does_not_require_core_requirements_context() -> None: + code = """ +import builtins + +real_import = builtins.__import__ + +def guarded_import(name, globals=None, locals=None, fromlist=(), level=0): + if name.startswith("specfact_cli.requirements"): + raise ModuleNotFoundError("blocked specfact_cli.requirements import") + return real_import(name, globals, locals, fromlist, level) + +builtins.__import__ = guarded_import + +from specfact_requirements.requirements.commands import app + +assert app is not None +""" + source_path = Path(__file__).resolve().parents[3] / "packages" / "specfact-requirements" / "src" + env = os.environ | {"PYTHONPATH": os.pathsep.join([source_path.as_posix(), os.environ.get("PYTHONPATH", "")])} + + result = subprocess.run([sys.executable, "-c", code], capture_output=True, check=False, text=True, env=env) + + assert result.returncode == 0, result.stderr + + @pytest.mark.integration def test_requirements_import_list_and_validate_commands_emit_json(tmp_path: Path) -> None: bundle_dir = _bundle_dir(tmp_path) From f9f93601d81dc31a2923e8ee46067b5ad2f2ae36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:07:02 +0000 Subject: [PATCH 05/15] chore(modules): ci sign changed modules --- packages/specfact-requirements/module-package.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 6da3ab4c..edde3e0e 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -20,3 +20,4 @@ category: project bundle_group_command: requirements integrity: checksum: sha256:8911ec1e0dad47257fcc803be46be868898d58fa6c3b6925b37dd8849dfd8f3a + signature: uKk5I0K4OvASQ4o/9tnYE3xztHrRgFYezh0g/HAVZNOKOGvuv6gbdFgncmHOcFBB8dZJsucvQ3MY0GIhaElwCg== From b549b3ff72491ba8e585016a0ff6a5ef7c1e93ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:34:47 +0000 Subject: [PATCH 06/15] chore(registry): publish changed modules [skip ci] --- registry/index.json | 15 +++++++++++++++ .../modules/specfact-requirements-0.1.1.tar.gz | Bin 0 -> 3978 bytes .../specfact-requirements-0.1.1.tar.gz.sha256 | 1 + .../specfact-requirements-0.1.1.tar.sig | 1 + 4 files changed, 17 insertions(+) create mode 100644 registry/modules/specfact-requirements-0.1.1.tar.gz create mode 100644 registry/modules/specfact-requirements-0.1.1.tar.gz.sha256 create mode 100644 registry/signatures/specfact-requirements-0.1.1.tar.sig diff --git a/registry/index.json b/registry/index.json index 6f65c42b..463aa029 100644 --- a/registry/index.json +++ b/registry/index.json @@ -91,6 +91,21 @@ "nold-ai/specfact-codebase" ], "description": "Official SpecFact code review bundle package." + }, + { + "id": "nold-ai/specfact-requirements", + "latest_version": "0.1.1", + "download_url": "modules/specfact-requirements-0.1.1.tar.gz", + "checksum_sha256": "67f0b4e94f78f7d826541229a201fb1add6c1716debac418b5635b30cfdd35de", + "tier": "official", + "publisher": { + "name": "nold-ai", + "email": "hello@noldai.com" + }, + "bundle_dependencies": [ + "nold-ai/specfact-project" + ], + "description": "Official SpecFact requirements evidence runtime bundle package." } ] } diff --git a/registry/modules/specfact-requirements-0.1.1.tar.gz b/registry/modules/specfact-requirements-0.1.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..82b6df09036ed33078c714deb980d764ffdc111d GIT binary patch literal 3978 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 literal 0 HcmV?d00001 diff --git a/registry/modules/specfact-requirements-0.1.1.tar.gz.sha256 b/registry/modules/specfact-requirements-0.1.1.tar.gz.sha256 new file mode 100644 index 00000000..a57f5f8e --- /dev/null +++ b/registry/modules/specfact-requirements-0.1.1.tar.gz.sha256 @@ -0,0 +1 @@ +67f0b4e94f78f7d826541229a201fb1add6c1716debac418b5635b30cfdd35de diff --git a/registry/signatures/specfact-requirements-0.1.1.tar.sig b/registry/signatures/specfact-requirements-0.1.1.tar.sig new file mode 100644 index 00000000..a9819cad --- /dev/null +++ b/registry/signatures/specfact-requirements-0.1.1.tar.sig @@ -0,0 +1 @@ +uKk5I0K4OvASQ4o/9tnYE3xztHrRgFYezh0g/HAVZNOKOGvuv6gbdFgncmHOcFBB8dZJsucvQ3MY0GIhaElwCg== From 5d2ddd99c09fc240fe043dda5b6726443ca00b67 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 8 Jul 2026 22:59:42 +0200 Subject: [PATCH 07/15] fix: address requirements module promotion review --- .../specfact-requirements/module-package.yaml | 5 +-- .../requirements/commands.py | 3 +- .../requirements/runtime.py | 38 ++++++++++++++++--- .../test_command_apps.py | 2 +- .../test_requirements_runtime.py | 29 ++++++++++++++ 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index edde3e0e..328a7e48 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-requirements -version: 0.1.1 +version: 0.1.2 commands: - requirements tier: official @@ -19,5 +19,4 @@ description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:8911ec1e0dad47257fcc803be46be868898d58fa6c3b6925b37dd8849dfd8f3a - signature: uKk5I0K4OvASQ4o/9tnYE3xztHrRgFYezh0g/HAVZNOKOGvuv6gbdFgncmHOcFBB8dZJsucvQ3MY0GIhaElwCg== + checksum: sha256:1102344ecfb377bdbd2d41585e78c82886e315436cfe8a7915dd4745d6639ddd diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py index e0c9ed1c..a16c6452 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py @@ -16,6 +16,7 @@ import_requirements_file_to_bundle, inspect_requirements_bundle_coverage, list_requirements_with_coverage, + normalize_requirement_context_profile, validate_requirements_bundle, ) @@ -39,7 +40,7 @@ def _format_supported(output_format: OutputFormat) -> bool: def _profile_supported(profile: str) -> bool: - return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES + return normalize_requirement_context_profile(profile) in KNOWN_REQUIREMENT_CONTEXT_PROFILES @beartype diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py index fbf684db..1ce2feb1 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py @@ -15,15 +15,20 @@ from specfact_cli.utils.bundle_loader import load_project_bundle, save_project_bundle -_REQUIREMENTS_INPUTS_FILE = "requirements.inputs.yaml" +_LEGACY_REQUIREMENTS_INPUTS_FILE = "requirements.inputs.yaml" +_REQUIREMENTS_INPUTS_FILE = "inputs.yaml" +_REQUIREMENTS_INPUTS_DIR = "requirements" _REQUIREMENTS_MODELS_MODULE = "specfact_cli.models.requirements" _REQUIREMENTS_CONTEXT_MODULE = "specfact_cli.requirements.context" KNOWN_REQUIREMENT_CONTEXT_PROFILES: frozenset[str] = frozenset( { + "api-first-team", "solo", + "solo-developer", "startup", "team", "enterprise", + "enterprise-full-stack", "strict", "solo_developer", "api_first_team", @@ -63,14 +68,33 @@ def _profile_is_supported(profile: str) -> bool: return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES +@beartype +@require(lambda profile: bool(profile.strip()), "profile must be non-empty") +@ensure(lambda result: bool(result.strip())) +def normalize_requirement_context_profile(profile: str) -> str: + """Return the core validator spelling for documented profile aliases.""" + return profile.replace("-", "_") + + def _requirements_sidecar_path(bundle_dir: Path) -> Path: - return bundle_dir / _REQUIREMENTS_INPUTS_FILE + return bundle_dir / "reports" / _REQUIREMENTS_INPUTS_DIR / _REQUIREMENTS_INPUTS_FILE + + +def _legacy_requirements_sidecar_path(bundle_dir: Path) -> Path: + return bundle_dir / _LEGACY_REQUIREMENTS_INPUTS_FILE + + +def _existing_requirements_sidecar_path(bundle_dir: Path) -> Path | None: + for candidate in (_requirements_sidecar_path(bundle_dir), _legacy_requirements_sidecar_path(bundle_dir)): + if candidate.exists(): + return candidate + return None def _load_bundle_with_requirements(bundle_dir: Path) -> ProjectBundle: bundle = load_project_bundle(bundle_dir) - sidecar = _requirements_sidecar_path(bundle_dir) - if not sidecar.exists(): + sidecar = _existing_requirements_sidecar_path(bundle_dir) + if sidecar is None: return bundle payload = yaml.safe_load(sidecar.read_text(encoding="utf-8")) if not isinstance(payload, dict): @@ -86,7 +110,9 @@ def _load_bundle_with_requirements(bundle_dir: Path) -> ProjectBundle: def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[Any]) -> None: model_helpers = _load_requirements_module(_REQUIREMENTS_MODELS_MODULE, "requirements models") payload = model_helpers.requirements_input_extension_payload(list(records)) - _requirements_sidecar_path(bundle_dir).write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + sidecar = _requirements_sidecar_path(bundle_dir) + sidecar.parent.mkdir(parents=True, exist_ok=True) + sidecar.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") @beartype @@ -151,7 +177,7 @@ def validate_requirements_bundle(bundle_dir: Path, *, profile: str = "startup") """Validate requirement context evidence usefulness for a bundle.""" bundle = _load_bundle_with_requirements(bundle_dir) context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") - return context_helpers.validate_requirement_context(bundle, profile=profile) + return context_helpers.validate_requirement_context(bundle, profile=normalize_requirement_context_profile(profile)) @beartype diff --git a/tests/integration/specfact_requirements/test_command_apps.py b/tests/integration/specfact_requirements/test_command_apps.py index a0b4afc4..320e5928 100644 --- a/tests/integration/specfact_requirements/test_command_apps.py +++ b/tests/integration/specfact_requirements/test_command_apps.py @@ -103,7 +103,7 @@ def test_requirements_import_list_and_validate_commands_emit_json(tmp_path: Path validate_result = runner.invoke( app, - ["validate", "--bundle", str(bundle_dir), "--profile", "enterprise", "--format", "json"], + ["validate", "--bundle", str(bundle_dir), "--profile", "enterprise-full-stack", "--format", "json"], ) assert validate_result.exit_code == 1 diff --git a/tests/unit/specfact_requirements/test_requirements_runtime.py b/tests/unit/specfact_requirements/test_requirements_runtime.py index 3cd8a7b7..8ee6e706 100644 --- a/tests/unit/specfact_requirements/test_requirements_runtime.py +++ b/tests/unit/specfact_requirements/test_requirements_runtime.py @@ -75,6 +75,35 @@ def test_validate_requirements_bundle_uses_profile_aware_core_validation(tmp_pat assert report.violations[0]["location"] == "requirements.inputs[REQ-165].evidence_links" +def test_validate_requirements_bundle_accepts_hyphenated_profile_alias(tmp_path: Path) -> None: + source = tmp_path / "requirements.json" + source.write_text(json.dumps([_requirement_record()]), encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + import_requirements_file_to_bundle(source, bundle_dir) + + report = validate_requirements_bundle(bundle_dir, profile="enterprise-full-stack") + + assert report.status == "failed" + assert report.violations[0]["location"] == "requirements.inputs[REQ-165].evidence_links" + + +def test_imported_requirements_survive_later_atomic_bundle_save(tmp_path: Path) -> None: + source = tmp_path / "requirements.json" + source.write_text(json.dumps([_requirement_record("REQ-PRESERVE", with_evidence=True)]), encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + import_requirements_file_to_bundle(source, bundle_dir) + + assert (bundle_dir / "reports" / "requirements" / "inputs.yaml").is_file() + assert not (bundle_dir / "requirements.inputs.yaml").exists() + + bundle = create_empty_project_bundle("bundle") + save_project_bundle(bundle, bundle_dir, atomic=True) + + listing = list_requirements_with_coverage(bundle_dir) + + assert [record["requirement_id"] for record in listing["requirements"]] == ["REQ-PRESERVE"] + + def test_list_requirements_with_coverage_is_machine_readable(tmp_path: Path) -> None: source = tmp_path / "requirements.json" source.write_text(json.dumps([_requirement_record("REQ-COVERED", with_evidence=True)]), encoding="utf-8") From de87e50d1638308a12d7c859faf23c4fe7bac189 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:00:05 +0000 Subject: [PATCH 08/15] chore(modules): auto-sign module manifests --- packages/specfact-requirements/module-package.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 328a7e48..1c3a9c16 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -20,3 +20,4 @@ category: project bundle_group_command: requirements integrity: checksum: sha256:1102344ecfb377bdbd2d41585e78c82886e315436cfe8a7915dd4745d6639ddd + signature: htRmMiXxy7lBVh7BfDVMkAv/ugzzAG7vAfPum4ZoGvHJT8nPUO+LR7FbnPqMkJUsp4qabP0S8XDWSvbqYLZzDA== From 7ea3d94722288ac2980cb762eaec5818e3932d94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:04:59 +0000 Subject: [PATCH 09/15] chore(registry): publish changed modules [skip ci] --- registry/index.json | 6 +++--- .../modules/specfact-requirements-0.1.2.tar.gz | Bin 0 -> 4220 bytes .../specfact-requirements-0.1.2.tar.gz.sha256 | 1 + .../specfact-requirements-0.1.2.tar.sig | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 registry/modules/specfact-requirements-0.1.2.tar.gz create mode 100644 registry/modules/specfact-requirements-0.1.2.tar.gz.sha256 create mode 100644 registry/signatures/specfact-requirements-0.1.2.tar.sig diff --git a/registry/index.json b/registry/index.json index 463aa029..5637e7f3 100644 --- a/registry/index.json +++ b/registry/index.json @@ -94,9 +94,9 @@ }, { "id": "nold-ai/specfact-requirements", - "latest_version": "0.1.1", - "download_url": "modules/specfact-requirements-0.1.1.tar.gz", - "checksum_sha256": "67f0b4e94f78f7d826541229a201fb1add6c1716debac418b5635b30cfdd35de", + "latest_version": "0.1.2", + "download_url": "modules/specfact-requirements-0.1.2.tar.gz", + "checksum_sha256": "5c390fa62078893b7b4d91684bdf83c359a30b5e0b074c61de20c5e088e6ceaf", "tier": "official", "publisher": { "name": "nold-ai", diff --git a/registry/modules/specfact-requirements-0.1.2.tar.gz b/registry/modules/specfact-requirements-0.1.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8f034c86d024a87f1fc36f8ac9ba6c5731b0e2cd GIT binary patch literal 4220 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 S+Saz~+J6DH^WYf(cmM$Uok}_Y literal 0 HcmV?d00001 diff --git a/registry/modules/specfact-requirements-0.1.2.tar.gz.sha256 b/registry/modules/specfact-requirements-0.1.2.tar.gz.sha256 new file mode 100644 index 00000000..fdfa5959 --- /dev/null +++ b/registry/modules/specfact-requirements-0.1.2.tar.gz.sha256 @@ -0,0 +1 @@ +5c390fa62078893b7b4d91684bdf83c359a30b5e0b074c61de20c5e088e6ceaf diff --git a/registry/signatures/specfact-requirements-0.1.2.tar.sig b/registry/signatures/specfact-requirements-0.1.2.tar.sig new file mode 100644 index 00000000..5da3c433 --- /dev/null +++ b/registry/signatures/specfact-requirements-0.1.2.tar.sig @@ -0,0 +1 @@ +htRmMiXxy7lBVh7BfDVMkAv/ugzzAG7vAfPum4ZoGvHJT8nPUO+LR7FbnPqMkJUsp4qabP0S8XDWSvbqYLZzDA== From a416a01220b661b78311a89ff89985e1a73db4df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:05:01 +0000 Subject: [PATCH 10/15] chore(modules): auto-sign module manifests --- packages/specfact-requirements/module-package.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 1c3a9c16..1427b1be 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-requirements -version: 0.1.2 +version: 0.1.3 commands: - requirements tier: official @@ -19,5 +19,5 @@ description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:1102344ecfb377bdbd2d41585e78c82886e315436cfe8a7915dd4745d6639ddd - signature: htRmMiXxy7lBVh7BfDVMkAv/ugzzAG7vAfPum4ZoGvHJT8nPUO+LR7FbnPqMkJUsp4qabP0S8XDWSvbqYLZzDA== + checksum: sha256:5c1ac04b525f9e332904d75299eb43860ce64ee6575d580097229b3539d3211c + signature: pk0KertcYzbnEZys6N0/KsZ40KrTcnsY8xzM4C6+ScvmSWUqQgAh++xGH16H+NoFq8b9c/HiEKpN3RRMUP6UAg== From faa648933be31e3d34b9c01109c63a8a2b846481 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Wed, 8 Jul 2026 23:05:41 +0200 Subject: [PATCH 11/15] fix: address requirements promotion review followups --- .../CHANGE_VALIDATION.md | 2 +- .../TDD_EVIDENCE.md | 2 +- .../specs/backlog-adapter/spec.md | 2 +- .../specfact-requirements/module-package.yaml | 5 +- .../requirements/commands.py | 9 +-- .../requirements/runtime.py | 54 +++++++++------ .../test_requirements_runtime.py | 68 +++++++++++++++---- 7 files changed, 95 insertions(+), 47 deletions(-) diff --git a/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md b/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md index d2719cf6..9a7988bb 100644 --- a/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md +++ b/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md @@ -56,7 +56,7 @@ - `hatch run check-bundle-imports` - `hatch run check-command-overview` - `hatch run check-command-contract` - - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file /Users/dom/git/nold-ai/specfact-cli-worktrees/feature/requirements-02-module-commands/resources/keys/module-signing-public.pem` + - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file resources/keys/module-signing-public.pem` - `hatch run contract-test` - `hatch run smart-test` (`849 passed, 2 warnings`) - `hatch run test` (`849 passed, 2 warnings`) diff --git a/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md b/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md index c4089dd4..01216ed7 100644 --- a/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md +++ b/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md @@ -31,7 +31,7 @@ - `hatch run check-bundle-imports` - `hatch run check-command-overview` - `hatch run check-command-contract` - - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file /Users/dom/git/nold-ai/specfact-cli-worktrees/feature/requirements-02-module-commands/resources/keys/module-signing-public.pem` + - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --public-key-file resources/keys/module-signing-public.pem` - `hatch run contract-test` - `hatch run smart-test` - `hatch run test` diff --git a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md b/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md index 8292f8eb..dae70768 100644 --- a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md +++ b/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md @@ -5,7 +5,7 @@ The system SHALL expose source-attributed backlog requirement snippets to requirements import workflows. -#### Scenario: Adapter returns acceptance criteria payload for extraction +#### Scenario: Adapter returns acceptance criteria payload for import - **GIVEN** a backlog item selected for requirement context import - **WHEN** requirements import receives adapter source fields diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 1427b1be..bb31cdd5 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-requirements -version: 0.1.3 +version: 0.1.4 commands: - requirements tier: official @@ -19,5 +19,4 @@ description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:5c1ac04b525f9e332904d75299eb43860ce64ee6575d580097229b3539d3211c - signature: pk0KertcYzbnEZys6N0/KsZ40KrTcnsY8xzM4C6+ScvmSWUqQgAh++xGH16H+NoFq8b9c/HiEKpN3RRMUP6UAg== + checksum: sha256:286853516f6b8949b9e8c6192ea90e28e2608d1beeb9a45962fefb739dbd645c diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py index a16c6452..2064958f 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py @@ -12,11 +12,10 @@ from icontract import ensure, require from specfact_requirements.requirements.runtime import ( - KNOWN_REQUIREMENT_CONTEXT_PROFILES, import_requirements_file_to_bundle, inspect_requirements_bundle_coverage, + is_requirement_context_profile_supported, list_requirements_with_coverage, - normalize_requirement_context_profile, validate_requirements_bundle, ) @@ -39,10 +38,6 @@ def _format_supported(output_format: OutputFormat) -> bool: return output_format in {OutputFormat.JSON, OutputFormat.TEXT} -def _profile_supported(profile: str) -> bool: - return normalize_requirement_context_profile(profile) in KNOWN_REQUIREMENT_CONTEXT_PROFILES - - @beartype @require(_format_supported, "output format must be supported") @ensure(lambda result: result is None) @@ -87,7 +82,7 @@ def import_command( @app.command("validate", help="Validate requirement context evidence usefulness.") @beartype @require(lambda bundle: bundle.is_dir(), "bundle must exist") -@require(_profile_supported, "profile must be a known requirement context profile") +@require(is_requirement_context_profile_supported, "profile must be a known requirement context profile") @require(_format_supported, "output format must be supported") @ensure(lambda result: result is None) def validate_command( diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py index 1ce2feb1..0d140d55 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py @@ -5,7 +5,7 @@ from collections.abc import Mapping, Sequence from importlib import import_module from pathlib import Path -from typing import Any, cast +from typing import Any, cast, get_args import yaml from beartype import beartype @@ -20,21 +20,6 @@ _REQUIREMENTS_INPUTS_DIR = "requirements" _REQUIREMENTS_MODELS_MODULE = "specfact_cli.models.requirements" _REQUIREMENTS_CONTEXT_MODULE = "specfact_cli.requirements.context" -KNOWN_REQUIREMENT_CONTEXT_PROFILES: frozenset[str] = frozenset( - { - "api-first-team", - "solo", - "solo-developer", - "startup", - "team", - "enterprise", - "enterprise-full-stack", - "strict", - "solo_developer", - "api_first_team", - "enterprise_full_stack", - } -) class RequirementsCoreUnavailableError(RuntimeError): @@ -64,8 +49,20 @@ def _has_attributes(result: Any, *attributes: str) -> bool: return all(hasattr(result, attribute) for attribute in attributes) -def _profile_is_supported(profile: str) -> bool: - return profile in KNOWN_REQUIREMENT_CONTEXT_PROFILES +def _profile_aliases(profiles: frozenset[str]) -> frozenset[str]: + aliases = set(profiles) + aliases.update(profile.replace("_", "-") for profile in profiles) + aliases.update(profile.replace("-", "_") for profile in profiles) + return frozenset(aliases) + + +def _known_requirement_context_profiles() -> frozenset[str]: + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + known_profiles = getattr(context_helpers, "KNOWN_REQUIREMENT_CONTEXT_PROFILES", None) + if known_profiles is None: + profile_type = context_helpers.RequirementContextValidationProfile + known_profiles = get_args(profile_type) + return _profile_aliases(frozenset(str(profile) for profile in known_profiles)) @beartype @@ -76,6 +73,17 @@ def normalize_requirement_context_profile(profile: str) -> str: return profile.replace("-", "_") +@beartype +@require(lambda profile: bool(profile.strip()), "profile must be non-empty") +@ensure(lambda result: isinstance(result, bool)) +def is_requirement_context_profile_supported(profile: str) -> bool: + """Return whether the paired core validator exposes this profile.""" + normalized = normalize_requirement_context_profile(profile) + return normalized in { + normalize_requirement_context_profile(candidate) for candidate in _known_requirement_context_profiles() + } + + def _requirements_sidecar_path(bundle_dir: Path) -> Path: return bundle_dir / "reports" / _REQUIREMENTS_INPUTS_DIR / _REQUIREMENTS_INPUTS_FILE @@ -112,7 +120,13 @@ def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[Any]) -> Non payload = model_helpers.requirements_input_extension_payload(list(records)) sidecar = _requirements_sidecar_path(bundle_dir) sidecar.parent.mkdir(parents=True, exist_ok=True) - sidecar.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + temporary_sidecar = sidecar.with_name(f".{sidecar.name}.tmp") + try: + temporary_sidecar.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + temporary_sidecar.replace(sidecar) + finally: + if temporary_sidecar.exists(): + temporary_sidecar.unlink() @beartype @@ -171,7 +185,7 @@ def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> A @beartype @require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") -@require(_profile_is_supported, "profile must be a known requirement context profile") +@require(is_requirement_context_profile_supported, "profile must be a known requirement context profile") @ensure(lambda result: isinstance(result, ValidationReport)) def validate_requirements_bundle(bundle_dir: Path, *, profile: str = "startup") -> ValidationReport: """Validate requirement context evidence usefulness for a bundle.""" diff --git a/tests/unit/specfact_requirements/test_requirements_runtime.py b/tests/unit/specfact_requirements/test_requirements_runtime.py index 8ee6e706..10592288 100644 --- a/tests/unit/specfact_requirements/test_requirements_runtime.py +++ b/tests/unit/specfact_requirements/test_requirements_runtime.py @@ -3,12 +3,18 @@ from __future__ import annotations import json +from collections.abc import Callable +from importlib import import_module from pathlib import Path +from types import ModuleType +import pytest from specfact_cli.common.bundle_factory import create_empty_project_bundle from specfact_cli.utils.bundle_loader import save_project_bundle +from specfact_requirements.requirements import runtime as requirements_runtime from specfact_requirements.requirements.runtime import ( + RequirementsCoreUnavailableError, import_requirements_file_to_bundle, list_requirements_with_coverage, validate_requirements_bundle, @@ -40,6 +46,15 @@ def _bundle_dir(tmp_path: Path) -> Path: return bundle_dir +def _block_runtime_import(module_name: str) -> Callable[[str], ModuleType]: + def blocked_import(name: str) -> ModuleType: + if name == module_name: + raise ImportError(f"blocked import: {name}") + return import_module(name) + + return blocked_import + + def test_import_requirements_file_to_bundle_preserves_valid_records_and_diagnostics(tmp_path: Path) -> None: source = tmp_path / "requirements.json" source.write_text( @@ -63,25 +78,14 @@ def test_import_requirements_file_to_bundle_preserves_valid_records_and_diagnost assert [record["requirement_id"] for record in listing["requirements"]] == ["REQ-165"] -def test_validate_requirements_bundle_uses_profile_aware_core_validation(tmp_path: Path) -> None: - source = tmp_path / "requirements.json" - source.write_text(json.dumps([_requirement_record()]), encoding="utf-8") - bundle_dir = _bundle_dir(tmp_path) - import_requirements_file_to_bundle(source, bundle_dir) - - report = validate_requirements_bundle(bundle_dir, profile="enterprise") - - assert report.status == "failed" - assert report.violations[0]["location"] == "requirements.inputs[REQ-165].evidence_links" - - -def test_validate_requirements_bundle_accepts_hyphenated_profile_alias(tmp_path: Path) -> None: +@pytest.mark.parametrize("profile", ["enterprise", "enterprise-full-stack"]) +def test_validate_requirements_bundle_uses_profile_aware_core_validation(tmp_path: Path, profile: str) -> None: source = tmp_path / "requirements.json" source.write_text(json.dumps([_requirement_record()]), encoding="utf-8") bundle_dir = _bundle_dir(tmp_path) import_requirements_file_to_bundle(source, bundle_dir) - report = validate_requirements_bundle(bundle_dir, profile="enterprise-full-stack") + report = validate_requirements_bundle(bundle_dir, profile=profile) assert report.status == "failed" assert report.violations[0]["location"] == "requirements.inputs[REQ-165].evidence_links" @@ -104,6 +108,42 @@ def test_imported_requirements_survive_later_atomic_bundle_save(tmp_path: Path) assert [record["requirement_id"] for record in listing["requirements"]] == ["REQ-PRESERVE"] +def test_requirements_runtime_raises_clear_error_when_core_context_is_unavailable( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + source = tmp_path / "requirements.json" + source.write_text(json.dumps([_requirement_record()]), encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + monkeypatch.setattr( + requirements_runtime, + "import_module", + _block_runtime_import("specfact_cli.requirements.context"), + ) + + with pytest.raises(RequirementsCoreUnavailableError, match="requirements context helpers"): + import_requirements_file_to_bundle(source, bundle_dir) + + with pytest.raises(RequirementsCoreUnavailableError, match="requirements context helpers"): + validate_requirements_bundle(bundle_dir) + + +def test_requirements_runtime_raises_clear_error_when_core_models_are_unavailable( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + bundle_dir = _bundle_dir(tmp_path) + sidecar = bundle_dir / "reports" / "requirements" / "inputs.yaml" + sidecar.parent.mkdir(parents=True) + sidecar.write_text("requirements: []\n", encoding="utf-8") + monkeypatch.setattr( + requirements_runtime, + "import_module", + _block_runtime_import("specfact_cli.models.requirements"), + ) + + with pytest.raises(RequirementsCoreUnavailableError, match="requirements models"): + list_requirements_with_coverage(bundle_dir) + + def test_list_requirements_with_coverage_is_machine_readable(tmp_path: Path) -> None: source = tmp_path / "requirements.json" source.write_text(json.dumps([_requirement_record("REQ-COVERED", with_evidence=True)]), encoding="utf-8") From 875cd85ff280f38eaabd08ed4b224bff19041555 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:14:34 +0000 Subject: [PATCH 12/15] chore(registry): publish changed modules [skip ci] --- .../specfact-requirements/module-package.yaml | 1 + registry/index.json | 6 +++--- .../modules/specfact-requirements-0.1.4.tar.gz | Bin 0 -> 4370 bytes .../specfact-requirements-0.1.4.tar.gz.sha256 | 1 + .../specfact-requirements-0.1.4.tar.sig | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 registry/modules/specfact-requirements-0.1.4.tar.gz create mode 100644 registry/modules/specfact-requirements-0.1.4.tar.gz.sha256 create mode 100644 registry/signatures/specfact-requirements-0.1.4.tar.sig diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index bb31cdd5..5cf05fde 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -20,3 +20,4 @@ category: project bundle_group_command: requirements integrity: checksum: sha256:286853516f6b8949b9e8c6192ea90e28e2608d1beeb9a45962fefb739dbd645c + signature: pBbt4tKSWfQfFZBQHK8Yuz206XVEGh1VF4nSt9LpWPCar/RV5deGbjpw2uyWJcX0dBikK58vo8jgVoU5I1tMAQ== diff --git a/registry/index.json b/registry/index.json index 5637e7f3..053d0b37 100644 --- a/registry/index.json +++ b/registry/index.json @@ -94,9 +94,9 @@ }, { "id": "nold-ai/specfact-requirements", - "latest_version": "0.1.2", - "download_url": "modules/specfact-requirements-0.1.2.tar.gz", - "checksum_sha256": "5c390fa62078893b7b4d91684bdf83c359a30b5e0b074c61de20c5e088e6ceaf", + "latest_version": "0.1.4", + "download_url": "modules/specfact-requirements-0.1.4.tar.gz", + "checksum_sha256": "a4e275705fce188e8c9075b59e455c87b941cf13524e91fa704c4944226d2d14", "tier": "official", "publisher": { "name": "nold-ai", diff --git a/registry/modules/specfact-requirements-0.1.4.tar.gz b/registry/modules/specfact-requirements-0.1.4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..393e03457ba7931459361b9af2e489b235a738b9 GIT binary patch literal 4370 zcmV+t5$*0DiwFq1y-sQZ|8sC0{baO2*E-@}NE_7jX0PP)X zbKAzzpZP0p@TEY9gh@)GBu?3J;>f8R+j8Vbl6o|lBXATEfdC2|WHGM(d-tBe0U$*w zv0}Ht2a^Eq_V)Jny-V4B>^^?-8eji|;DBV0KFc)}*QU?maBsi-&Yq8kaPF!D^H2Pz&;&h~k3(9UhFTpX?fqLHb4qjC zPKNuB(C{JUKP%#Rn*zM;{a1R$ctxftPNHCd!^f@M(7Pg89wzY=4c(Eu-}95z3dcb{ z?IAQk#z-2HY>JY_BJ@KX_0nP=7yX>iL+GizI`E)Se_2F0!OZJxTGYN$fC&Nf|>AkoT9wzs!r(6y-}i zKA23$N0Xz2y@SzYF_|A7?;p>P$&o)99ghh<9+L498Bc~s!Dvp%{21>a98bmzvX~$4 z9S8GZvVXv6lZO{ErbPzFmVP^@`}Bu5KP`S-Jp0$TKmOx~qhE^OKu&*tfAalu^#0j? z{DvOCNPl|$6lagm-X8?y`}wcw$8oX#>7V}3!{FQS@`rioZlxGc+R_gR#yB0>Qd#`Suhhoi{z&z?0;wfze4+eFx)$ubkf_||Azg)k27$q{h#dB?EfK*@9h5r+5fYu*_WvX#1jv~ z>Mycng<846HEvX12!*RO$tc2U+B1CycmlpcCv=_t@9h51Va@(eM#H_%{y&iY&)V!i++^!C31iwh0iFHt?0;wfyQ{PRHT$oW{qKtZN25cK z|62S%+Uw%~hqC`qrO97n)^d>LXpv;qS^#3b6XcreHin8v;CYLJbqIiMR|Y?h6N*_| zMBeMkXTRo2EXI?#SgDb3Xm$d(;zf$-G79JF-D|iNFX%cA;|ult8v&gJefYZ83y8)I zK3=tbs2REqgum5&AnG-Vb3nhZ_Pe{Iqv|6Pj@?6Wi5p{8sqblN@vz{PSd_dyN}pnl7hGJsn~FwcsRSrJ)k8A`8F#g zDHJ(#LMd=rKKo>`5njUkdOX&$lv`79S_ez3kF3Ffy)C~DaBziTp@2VHV(a`y5HKJM z&ppz787v9nF&@67={*qwVn=v24=^B>7ZIJxPvB1UDv60BHcVC_a3)?y2@Y)e zIYmL}(+@e#xWu0K(QQ;<00kAoMU4Wp8EOzf3}u(#AL1_)o3WZ36st5>c=UmMz!Y>g z?xUQ^=p|X_oD&>JLl9)=w@e4Z5YBc5VYHWk+>nx0Zab~(F04;5>1K*P@o6^}V}>Uz zOqN1oTS7$u!&JE&MTw6igPAkpCmD;QVwxbV`od)An9EM4dBpONmDLbI zl*_oSpK`r67%)Hs1_+LbZ%uGXA8~z~T;jF72U>{RXE@5a1;GKHM?}6B$ICeM@h{)J ze4)EV6Popc|*RcJVg;d{~h}Gn^#IGuf{RUcMyyo-M zDgyxfo}*b)bAj?SH2~O<`wECAsx z4C_SUZ{W;VNfX9^M2;|}*k8gsu4i{~K)zJRfrm$5sO;s-DpeYk<_&^OW zy~)^*ihv;X0tf}0&DK&%1X5puDBhdxq(qwHbW7N)CDWks%(!a`U0rNih`l)kRfE4y zIZ@busj~8CyQDiEyyR3ZinE=vKK5qMQ-j{sY;^UXuKv^2f4b|l)qi9T{I(vD?e!n_ zV~Fj*<(*!JonYWm*8E6@kJlK zfwfrVB`Y-iZKV_>6F1^_D0eItt^3HwIk*BBgz_4It{Cy({QeOIBeP!QN1-s!xvLUV z3Q2MMEjRC*XI+({w47K5*JnKbYnfZn5T;5`g82$0Q?uEfCsFaVAB?X^?ICN@d2#an zH&1`@&Q5-O_x$YS<;knJZ@lNPUcZBnXU|`puz21mfeA;Q|8)SXw{_a>BfoooR)=37 z`SSF;lNbEsoq*a#zij}Xp1xu(-6jaNKBibzr$jZZ+9mxo$;i7HUqSTEJk*mcOEO!E z5x*(RWM?cO575UYi4k40pL_!lRx`_pDP&6eFuW*A*h1DseTAQ;;XDeVu*HWbeqqlu zHffQiNiOcV0}HYsaH83IS}S;%B36Ya+)#4ulawNniRJr4IA>2wK&$)$=xHX?7gnor zQQOX|^w1;WAkZg?{SC6rjHy&7Kp}sL;|r65gW-4}|89j+RFzzNEkknaX~KpaBuduT zzB&Mzat2LO$vfIAl2Ur?gVbzg(d)duyoaO64s(bKC@Uh`0(xI5Mn@`Q_OML#Iwdab z)?4D-BiAgfAOWvwX>`oGTRR4(yp^k{MHqC5!?SQ+P*Q^CiuJGMi;~S+_XtqfSUJ6q z^hB{meaqG_f$E*AOfTp`oC6l}KIhe~-#{M|zE815^N~PBP6P&p=X$zRediV_%j>C^ zpbOFn!$6j4K;8iHL~cUnL2JNe z>Y`Leieb1_pVfEnR%HLFcvYI%*J7#$)wU1rNZ8dyjN@zYYk8h<8fspH70i_!r@e?T zlQ%k1&*V+B8-Nc}+i_YdA#oB9$SS4l&50lmO4gGUB#{o%dAe<^XKS34E`ZO{3<}yX zjnDPBbaQ|Np!uVSIc>bSmG}h%4i-V}K#5kFi|3Fwir9nG&CuXN!7=EPcDq)un&c~0 zX-PkDBY=ci!;>h<3AP}~xorip(8U-@yGp`!cDGYCIJ^Re^`vNF*$tu(mYEk(gb3(c z25Tn`$Je_l?O;WJ5ja)Z7sEi6qtIif+SqY`DSV7Jx~Usp zF;%5KHFAOd^&gAdZSJ{RnjhnrEUq@% zR4Ai|RmsIrH+*He3NHp!aoIqCYJ@heq`W~`TEeL69z+F4Y>-*jJDq{HYYITapc(8S zDzzdf3~RBN%FXdY1b()J;j-+x#{|a)moy0gmzh=2#o)+t^m-7eSGAY!lwzKvQ}^u` z)+Y)3MrcPSn3#ENs3j;wE8)Z{M6H_t{hqZ)m7X?#E7IC`_fL$tu591x68h*+qh#REf2^f2n1tR&W21IxE4nlUd3X6$f|o*Y7*lt3$x3F%m#$K|c_W^0fOExpaz#*T3SK(W;$F5x z@ZW2xAER)`n(pPHorl&2dEx4&tS~-L=b;BJ+uC0j%5@jgOV* zwc4um?b<_pmtAQ0`Bj)t^CY{C< zt^haezc1Hwwm_|0Fua}ys;BaPfYoegW9W7ts4@*FBzi@%3p3gg?vE|=riw0xee73$D#G?wds)gpyVuhG zk}aiYnTI!DL)6G}m+1L-5F;|}mI9t6yJE+!&;TX~NT#KKzJ~1qD}54sbIpBE)t1{2 z>qhipFo5hwy{#ZNOg-1ZMf0c(gV7F@JFT(6-V{NxJ>YQ?2X^b^QsnsbcC=X=hNj9tU9GrEUW7%R6Mkr5RPa~Fxp71SfbInez(-H z^SK!Lj;4tw6h~g&K=Jc|anxf$WND;!@5S6=lT;_DZt`S|^E=v_ziJk1A*tpWP11WA zfX~fg8;j|cpAFpFUS88wwSIn9%~2iroQ=%%UcJx*zI(`ch&%O$zmt5-xq~BqqKu=K zUjMI*nJPn?&dpBzQOyF@8y{GNquqYfeVw8YMgU>{qOU%l1%t-_z)*`D`pwxiq+lq` zN&H#MSm}@XNfA^18^Us$cwPSuA)l!X7*}u`0T`p|aBj@To!Z}j2xId1e?d*xhK zzA!kxtj3FT`B46V;=JXYcTygP|F@|P-l!6bwYyH{?&_}Y>aOnUuI}ot?&_}Y>aIus M1*u>=7XWww00*_T1poj5 literal 0 HcmV?d00001 diff --git a/registry/modules/specfact-requirements-0.1.4.tar.gz.sha256 b/registry/modules/specfact-requirements-0.1.4.tar.gz.sha256 new file mode 100644 index 00000000..5f49f44a --- /dev/null +++ b/registry/modules/specfact-requirements-0.1.4.tar.gz.sha256 @@ -0,0 +1 @@ +a4e275705fce188e8c9075b59e455c87b941cf13524e91fa704c4944226d2d14 diff --git a/registry/signatures/specfact-requirements-0.1.4.tar.sig b/registry/signatures/specfact-requirements-0.1.4.tar.sig new file mode 100644 index 00000000..d151abf6 --- /dev/null +++ b/registry/signatures/specfact-requirements-0.1.4.tar.sig @@ -0,0 +1 @@ +pBbt4tKSWfQfFZBQHK8Yuz206XVEGh1VF4nSt9LpWPCar/RV5deGbjpw2uyWJcX0dBikK58vo8jgVoU5I1tMAQ== From 8da5bf5ba3db4f87986921d0c8e9133a76fcc6f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:14:35 +0000 Subject: [PATCH 13/15] chore(modules): auto-sign module manifests --- packages/specfact-requirements/module-package.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index bb31cdd5..5cf05fde 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -20,3 +20,4 @@ category: project bundle_group_command: requirements integrity: checksum: sha256:286853516f6b8949b9e8c6192ea90e28e2608d1beeb9a45962fefb739dbd645c + signature: pBbt4tKSWfQfFZBQHK8Yuz206XVEGh1VF4nSt9LpWPCar/RV5deGbjpw2uyWJcX0dBikK58vo8jgVoU5I1tMAQ== From 26f43f5822d841a1cb9e1de120ee3b2716754094 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:15:37 +0000 Subject: [PATCH 14/15] chore(modules): auto-sign module manifests --- packages/specfact-requirements/module-package.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index 5cf05fde..e465cb26 100644 --- a/packages/specfact-requirements/module-package.yaml +++ b/packages/specfact-requirements/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-requirements -version: 0.1.4 +version: 0.1.5 commands: - requirements tier: official @@ -19,5 +19,5 @@ description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:286853516f6b8949b9e8c6192ea90e28e2608d1beeb9a45962fefb739dbd645c - signature: pBbt4tKSWfQfFZBQHK8Yuz206XVEGh1VF4nSt9LpWPCar/RV5deGbjpw2uyWJcX0dBikK58vo8jgVoU5I1tMAQ== + checksum: sha256:d200aaecff7760ee660dd984d70d417b56960879385ebd745ddf48f08cee1b0d + signature: 3cXSjTAnhQHUcvDsM4pYwp8OxJ/gAvNtZD3H0zCMqOyuHW5axSeCJcpGDqjno1YLAq+v4B1PL5HYuJkAUeZQAQ== From bbc0e6922c5bbed46e4d1a9d4a8c98bb4dd5e453 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:16:30 +0000 Subject: [PATCH 15/15] chore(registry): publish changed modules [skip ci] --- registry/index.json | 6 +++--- .../modules/specfact-requirements-0.1.5.tar.gz | Bin 0 -> 4371 bytes .../specfact-requirements-0.1.5.tar.gz.sha256 | 1 + .../specfact-requirements-0.1.5.tar.sig | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 registry/modules/specfact-requirements-0.1.5.tar.gz create mode 100644 registry/modules/specfact-requirements-0.1.5.tar.gz.sha256 create mode 100644 registry/signatures/specfact-requirements-0.1.5.tar.sig diff --git a/registry/index.json b/registry/index.json index 053d0b37..9844978d 100644 --- a/registry/index.json +++ b/registry/index.json @@ -94,9 +94,9 @@ }, { "id": "nold-ai/specfact-requirements", - "latest_version": "0.1.4", - "download_url": "modules/specfact-requirements-0.1.4.tar.gz", - "checksum_sha256": "a4e275705fce188e8c9075b59e455c87b941cf13524e91fa704c4944226d2d14", + "latest_version": "0.1.5", + "download_url": "modules/specfact-requirements-0.1.5.tar.gz", + "checksum_sha256": "fd3f684be7d8ba76c16296c00e44f7ab05dbddf00b90bb716d5b9882bbc6745c", "tier": "official", "publisher": { "name": "nold-ai", diff --git a/registry/modules/specfact-requirements-0.1.5.tar.gz b/registry/modules/specfact-requirements-0.1.5.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..12bc5eeb5548acb19601c90d298925b75b20563b GIT binary patch literal 4371 zcmV+u5$x_CiwFoVzD{Za|8sC0{baO2*E-@}OE_7jX0PP)X zcbmr2pY<#D@TJfzVac*ASLdoHY2q}=P3%0|ra3;GHLNUR1cZlU)z|<1&OTuQ$+FWp z$t`?{1nkbv&dz&Aj=gPf-+O^C9uw@5Wb2DueSWR_?DvPe#drFA(BB#CY@v&-JGk;R z!wD?E_0PO^_R%8q7i2mZ?+-`2qv8J89`5Z9$AeL4tG)j3uQVp^47*t`As=%;Aqx^_ z>GmS>@__VW?4IFMVlVL`_)PiV+hsC77>@?!PkIe@K>7xwot@!mIPQ;zTm7B>Xgu0N z{kxR^BoD)N3h=u3-{}?N1(~2Q3cMcnw;Q>kb54@fkHQJ++XH*laihfohh94AAk;(J z$jm3n1Vyu%@A^3C#Q90!r*n7)L$#IQo-D8*OwgPJL3EEk#=Z@pJ12SQ1;p`4OhS)@ zu1~~DH6-IC`as;Q6Z>()@DqZQY#9@F2$s>vg2=f~Q6mVL zh$>se_lTr?bzGd0(BA}8K>cjS4q-ThgA-z8*l z()R#n>Ys)<%M&=Zq5H?{4{z>=^Ouj`y5|qm=ezN{Px1cI#nbK6`{#$*pAUzR`+q%n z{_$v;KmL7$FJ6-ePu=*}haW$L(cs;)`yc;xzWdAI#k0}lclpz^`)|pgFYmvcPFoxE zl`s37@c)K&|8LOxe|O^ljT`;H(P-F;=0Df}OA}X_Sx0;0IDY77j$_BmzsvvY4~Bac z|8I9VZ2iAGxeUW-_F1H>!b1TY#&tT5gM+|v&=kEl97ni?j$<6tSLzmC8`M+m8?6dy z?SGs9`5yB>d%OFs1JK(4y8XY6GjOf_-y2r!|G2-`Z|(mb+5cB%voBH&h$SAl)t@EN z0yT1jt=-7H5b_sslw=6UaYy$V;0gE&tTNp@W~kK02f|A<6{?d-CSsk2mxQ z-3koqfJ+Fs>=5lY^j}WRISJya@r2Q|tI+Nux|pEgHGIl997YaKPE!YH$w0nIazP47 zΞ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