diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 783241ae..82206bde 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -5,7 +5,7 @@ GEM public_suffix (>= 2.0.2, < 8.0) base64 (0.3.0) colorator (1.1.0) - concurrent-ruby (1.3.6) + concurrent-ruby (1.3.7) csv (3.3.5) em-websocket (0.5.3) eventmachine (>= 0.12.9) diff --git a/docs/bundles/requirements/overview.md b/docs/bundles/requirements/overview.md index 7a43e94f..4d7076ee 100644 --- a/docs/bundles/requirements/overview.md +++ b/docs/bundles/requirements/overview.md @@ -18,7 +18,7 @@ planning or product-management system. ## Prerequisites -- SpecFact CLI with core requirements context helpers +- SpecFact CLI `>=0.52.0,<1.0.0` - 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 @@ -30,10 +30,10 @@ 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 | +| `import` | Import local records or native OpenSpec/Spec Kit evidence into a project bundle | +| `validate` | Validate attached requirement context against the selected or layered profile | +| `list` | List attached requirement records with optional coverage and core gate counts | +| `coverage` | Print coverage and core gate-finding counts | ## Input shape @@ -69,11 +69,32 @@ Each record follows the core `RequirementInput` model and must include ```bash specfact requirements import --from-file requirements.json --bundle .specfact/projects/shop --format json +specfact requirements import --from-openspec openspec/changes/widget-evidence --bundle .specfact/projects/shop --format json +specfact requirements import --from-speckit specs/001-widget-rendering --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 ``` +For native imports, a source path is optional. `--from-openspec` discovers a +single conventional change below `openspec/changes/`; `--from-speckit` +discovers a single feature below `specs/`. Multiple or missing candidates are a +clear error so the command never guesses. Pass a path whenever the project has +more than one candidate. + +Native imports are read-only and supported only for core's default, +fixture-backed OpenSpec and Spec Kit Markdown profiles. A core +`unsupported-source-schema` diagnostic is returned unchanged, and no partial +requirements sidecar is written. The bundle does not infer upstream tool +versions, fall back to another parser, or add upstream metadata. + +When `--profile` is omitted, validation uses the effective layered core +configuration. Explicit `--profile` still wins. Core findings such as +`scenario-unverified`, `stale-import`, `source-missing`, +`ambiguous-mapping`, and `unsupported-profile-field` remain machine-readable; +the list and coverage commands include their counts without re-evaluating the +gates. + ## Storage The command runtime rehydrates the core `requirements.inputs` extension before @@ -87,6 +108,7 @@ The bundle is read-first and evidence-focused. - It imports and normalizes source-attributed requirement context. - It reports validation findings and coverage gaps. +- It delegates parsing, hashing, compatibility, and gate evaluation to core. - It does not expose authoring templates. - It does not perform bidirectional backlog sync or ceremony automation. diff --git a/docs/reference/commands.generated.json b/docs/reference/commands.generated.json index 21294e57..8a7d726d 100644 --- a/docs/reference/commands.generated.json +++ b/docs/reference/commands.generated.json @@ -190,7 +190,13 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog ceremony flow", "deprecated": false, @@ -206,7 +212,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog ceremony pi-summary", "deprecated": false, @@ -222,7 +234,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog ceremony planning", "deprecated": false, @@ -238,7 +256,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog ceremony refinement", "deprecated": false, @@ -252,7 +276,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog ceremony standup", "deprecated": false, @@ -268,7 +298,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog daily", "deprecated": false, @@ -359,7 +395,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ITEM_ID", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog delta impact", "deprecated": false, @@ -396,7 +438,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER_ARG", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact backlog delta status", "deprecated": false, @@ -500,7 +548,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "ADAPTER", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact backlog refine", "deprecated": false, @@ -665,7 +719,13 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact code drift detect", "deprecated": false, @@ -683,7 +743,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact code import", "deprecated": false, @@ -737,7 +803,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact code import from-code", "deprecated": false, @@ -975,7 +1047,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "FILES", + "nargs": -1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact code review review run", "deprecated": false, @@ -1043,7 +1121,18 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE_NAME", + "nargs": 1, + "required": true + }, + { + "name": "REPO_PATH", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact code validate sidecar init", "deprecated": false, @@ -1057,7 +1146,18 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE_NAME", + "nargs": 1, + "required": true + }, + { + "name": "REPO_PATH", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact code validate sidecar run", "deprecated": false, @@ -1113,7 +1213,13 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact govern enforce sdd", "deprecated": false, @@ -1164,7 +1270,13 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "PATCH_FILE", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact govern patch apply", "deprecated": false, @@ -1566,7 +1678,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "BUNDLE", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact project sync intelligent", "deprecated": false, @@ -1735,7 +1853,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "SOURCE_PATH", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact requirements import", "deprecated": false, @@ -1744,7 +1868,9 @@ "options": [ "--bundle", "--format", - "--from-file" + "--from-file", + "--from-openspec", + "--from-speckit" ], "owner_package": "nold-ai/specfact-requirements", "owner_repo": "nold-ai/specfact-cli-modules", @@ -1811,7 +1937,18 @@ ] }, { - "arguments": [], + "arguments": [ + { + "name": "OLD_SPEC", + "nargs": 1, + "required": true + }, + { + "name": "NEW_SPEC", + "nargs": 1, + "required": true + } + ], "bare_invocation": "executes", "command": "specfact spec backward-compat", "deprecated": false, @@ -1825,7 +1962,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "SPEC_PATH", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact spec generate-tests", "deprecated": false, @@ -1865,7 +2008,13 @@ "subcommands": [] }, { - "arguments": [], + "arguments": [ + { + "name": "SPEC_PATH", + "nargs": 1, + "required": false + } + ], "bare_invocation": "executes", "command": "specfact spec validate", "deprecated": false, diff --git a/docs/reference/commands.generated.md b/docs/reference/commands.generated.md index 315987f9..44291b6e 100644 --- a/docs/reference/commands.generated.md +++ b/docs/reference/commands.generated.md @@ -21,32 +21,32 @@ This file is generated from the current module command trees. Do not edit by han | `specfact backlog auth github` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --base-url, --client-id, --scopes; args: - | - | | | `specfact backlog auth status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | - | | | `specfact backlog ceremony` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | flow, pi-summary, planning, refinement, standup | | -| `specfact backlog ceremony flow` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony pi-summary` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony planning` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony refinement` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | - | | -| `specfact backlog ceremony standup` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog daily` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --annotations, --assignee, --blockers, --blockers-first, --comments, --copilot-export, --first-comments, --first-issues, --github-token, --id, --interactive, --iteration, --labels, --last-comments, --last-issues, --limit, --mode, --no-show-unassigned, --patch, --post, --release, --repo-name, --repo-owner, --search, --show-unassigned, --sprint, --state, --suggest-next, --summarize, --summarize-to, --tags, --today, --unassigned-only, --yesterday; args: - | - | | +| `specfact backlog ceremony flow` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony pi-summary` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony planning` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony refinement` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: ADAPTER (required) | - | | +| `specfact backlog ceremony standup` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog daily` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --annotations, --assignee, --blockers, --blockers-first, --comments, --copilot-export, --first-comments, --first-issues, --github-token, --id, --interactive, --iteration, --labels, --last-comments, --last-issues, --limit, --mode, --no-show-unassigned, --patch, --post, --release, --repo-name, --repo-owner, --search, --show-unassigned, --sprint, --state, --suggest-next, --summarize, --summarize-to, --tags, --today, --unassigned-only, --yesterday; args: ADAPTER (required) | - | | | `specfact backlog delta` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | cost-estimate, impact, rollback-analysis, status | | | `specfact backlog delta cost-estimate` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | -| `specfact backlog delta impact` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --template; args: - | - | | +| `specfact backlog delta impact` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --template; args: ITEM_ID (required) | - | | | `specfact backlog delta rollback-analysis` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | -| `specfact backlog delta status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --repo-name, --repo-owner, --since, --template; args: - | - | | +| `specfact backlog delta status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --repo-name, --repo-owner, --since, --template; args: ADAPTER_ARG | - | | | `specfact backlog diff` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | | `specfact backlog init-config` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --force; args: - | - | | | `specfact backlog map-fields` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-base-url, --ado-framework, --ado-org, --ado-project, --ado-token, --github-project-id, --github-project-v2-id, --github-type-field-id, --github-type-option, --non-interactive, --provider, --reset; args: - | - | | | `specfact backlog promote` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --item-id, --project-id, --template, --to-status; args: - | - | | -| `specfact backlog refine` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --assignee, --auto-accept-high-confidence, --auto-bundle, --bundle, --check-dor, --custom-field-mapping, --export-to-tmp, --first-comments, --first-issues, --framework, --github-token, --id, --ignore-refined, --import-from-tmp, --iteration, --labels, --last-comments, --last-issues, --limit, --no-ignore-refined, --no-preview, --openspec-comment, --persona, --preview, --release, --repo-name, --repo-owner, --search, --sprint, --state, --tags, --template, --tmp-file, --write; args: - | - | | +| `specfact backlog refine` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --assignee, --auto-accept-high-confidence, --auto-bundle, --bundle, --check-dor, --custom-field-mapping, --export-to-tmp, --first-comments, --first-issues, --framework, --github-token, --id, --ignore-refined, --import-from-tmp, --iteration, --labels, --last-comments, --last-issues, --limit, --no-ignore-refined, --no-preview, --openspec-comment, --persona, --preview, --release, --repo-name, --repo-owner, --search, --sprint, --state, --tags, --template, --tmp-file, --write; args: ADAPTER (required) | - | | | `specfact backlog sync` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --force-baseline-overwrite, --output-format, --project-id, --template; args: - | - | | | `specfact backlog verify-readiness` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --target-items, --template; args: - | - | | | `specfact code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-completion, --show-completion; args: - | analyze, drift, import, repro, validate | | | `specfact code analyze` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | contracts | | | `specfact code analyze contracts` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --bundle, --repo; args: - | - | | | `specfact code drift` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | detect | | -| `specfact code drift detect` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --format, --out, --repo; args: - | - | | -| `specfact code import` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: - | from-bridge, from-code | | +| `specfact code drift detect` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --format, --out, --repo; args: BUNDLE | - | | +| `specfact code import` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | from-bridge, from-code | | | `specfact code import from-bridge` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --adapter, --dry-run, --force, --out-branch, --repo, --report, --write; args: - | - | | -| `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: - | - | | +| `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | - | | | `specfact code repro` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --budget, --crosshair-per-path-timeout, --crosshair-required, --fail-fast, --fix, --out, --repo, --sidecar, --sidecar-bundle, --verbose; args: - | setup | | | `specfact code repro setup` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-crosshair, --repo; args: - | - | | | `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | review | | @@ -59,17 +59,17 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code review review rules init` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | | `specfact code review review rules show` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | | `specfact code review review rules update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | -| `specfact code review review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --preview-fixes, --scope, --score-only, --suppress-noise, --with-mutation; args: - | - | | +| `specfact code review review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --preview-fixes, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | | `specfact code validate` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | sidecar | | | `specfact code validate sidecar` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | init, run | | -| `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | - | | -| `specfact code validate sidecar run` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --no-run-crosshair, --no-run-specmatic, --run-crosshair, --run-specmatic; args: - | - | | +| `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | +| `specfact code validate sidecar run` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --no-run-crosshair, --no-run-specmatic, --run-crosshair, --run-specmatic; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | | `specfact govern` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --install-completion, --show-completion; args: - | enforce, patch | | | `specfact govern enforce` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | -; args: - | sdd, stage | | -| `specfact govern enforce sdd` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --no-interactive, --out, --output-format, --sdd; args: - | - | | +| `specfact govern enforce sdd` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --no-interactive, --out, --output-format, --sdd; args: BUNDLE | - | | | `specfact govern enforce stage` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --preset; args: - | - | | | `specfact govern patch` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | -; args: - | apply | | -| `specfact govern patch apply` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --dry-run, --write, --yes; args: - | - | | +| `specfact govern patch apply` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --dry-run, --write, --yes; args: PATCH_FILE (required) | - | | | `specfact project` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --install-completion, --show-completion; args: - | devops-flow, export, export-roadmap, health-check, import, init-personas, link-backlog, lock, locks, merge, regenerate, resolve-conflict, snapshot, sync, unlock, version | | | `specfact project devops-flow` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --action, --bundle, --no-interactive, --project-name, --repo, --stage, --verbose; args: - | - | | | `specfact project export` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --list-personas, --no-interactive, --out, --output, --output-dir, --persona, --repo, --stdout, --template; args: - | - | | @@ -86,7 +86,7 @@ This file is generated from the current module command trees. Do not edit by han | `specfact project snapshot` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --output, --project-name, --repo; args: - | - | | | `specfact project sync` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | -; args: - | bridge, intelligent, repository | | | `specfact project sync bridge` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --adapter, --add-progress-comment, --ado-base-url, --ado-org, --ado-project, --ado-token, --ado-work-item-type, --all, --backlog-ids, --backlog-ids-file, --bidirectional, --bundle, --change-ids, --code-repo, --ensure-compliance, --export-to-tmp, --external-base-path, --feature, --github-token, --import-from-tmp, --include-archived, --interactive, --interval, --mode, --no-add-progress-comment, --no-gh-cli, --no-include-archived, --no-sanitize, --no-track-code-changes, --no-update-existing, --overwrite, --repo, --repo-name, --repo-owner, --sanitize, --target-repo, --tmp-file, --track-code-changes, --update-existing, --use-gh-cli, --watch; args: - | - | | -| `specfact project sync intelligent` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --code-to-spec, --repo, --spec-to-code, --tests, --watch; args: - | - | | +| `specfact project sync intelligent` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --code-to-spec, --repo, --spec-to-code, --tests, --watch; args: BUNDLE | - | | | `specfact project sync repository` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --confidence, --interval, --repo, --target, --watch; args: - | - | | | `specfact project unlock` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --repo, --section; args: - | - | | | `specfact project version` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | -; args: - | bump, check, set | | @@ -95,11 +95,11 @@ This file is generated from the current module command trees. Do not edit by han | `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 import` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --from-file, --from-openspec, --from-speckit; args: SOURCE_PATH | - | | | `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: - | - | | +| `specfact spec backward-compat` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | -; args: OLD_SPEC (required), NEW_SPEC (required) | - | | +| `specfact spec generate-tests` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --out, --output; args: SPEC_PATH | - | | | `specfact spec mock` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --examples, --no-interactive, --port, --spec, --strict; args: - | - | | -| `specfact spec validate` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --no-interactive, --previous; args: - | - | | +| `specfact spec validate` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --no-interactive, --previous; args: SPEC_PATH | - | | diff --git a/llms.txt b/llms.txt index 2cd332d2..f213369b 100644 --- a/llms.txt +++ b/llms.txt @@ -25,32 +25,32 @@ This file is generated from the current module command trees. Do not edit by han | `specfact backlog auth github` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --base-url, --client-id, --scopes; args: - | - | | | `specfact backlog auth status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | - | | | `specfact backlog ceremony` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | flow, pi-summary, planning, refinement, standup | | -| `specfact backlog ceremony flow` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony pi-summary` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony planning` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog ceremony refinement` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | - | | -| `specfact backlog ceremony standup` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: - | - | | -| `specfact backlog daily` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --annotations, --assignee, --blockers, --blockers-first, --comments, --copilot-export, --first-comments, --first-issues, --github-token, --id, --interactive, --iteration, --labels, --last-comments, --last-issues, --limit, --mode, --no-show-unassigned, --patch, --post, --release, --repo-name, --repo-owner, --search, --show-unassigned, --sprint, --state, --suggest-next, --summarize, --summarize-to, --tags, --today, --unassigned-only, --yesterday; args: - | - | | +| `specfact backlog ceremony flow` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony pi-summary` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony planning` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog ceremony refinement` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: ADAPTER (required) | - | | +| `specfact backlog ceremony standup` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --mode; args: ADAPTER (required) | - | | +| `specfact backlog daily` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --annotations, --assignee, --blockers, --blockers-first, --comments, --copilot-export, --first-comments, --first-issues, --github-token, --id, --interactive, --iteration, --labels, --last-comments, --last-issues, --limit, --mode, --no-show-unassigned, --patch, --post, --release, --repo-name, --repo-owner, --search, --show-unassigned, --sprint, --state, --suggest-next, --summarize, --summarize-to, --tags, --today, --unassigned-only, --yesterday; args: ADAPTER (required) | - | | | `specfact backlog delta` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | cost-estimate, impact, rollback-analysis, status | | | `specfact backlog delta cost-estimate` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | -| `specfact backlog delta impact` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --template; args: - | - | | +| `specfact backlog delta impact` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --template; args: ITEM_ID (required) | - | | | `specfact backlog delta rollback-analysis` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | -| `specfact backlog delta status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --repo-name, --repo-owner, --since, --template; args: - | - | | +| `specfact backlog delta status` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --repo-name, --repo-owner, --since, --template; args: ADAPTER_ARG | - | | | `specfact backlog diff` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --project-id, --template; args: - | - | | | `specfact backlog init-config` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --force; args: - | - | | | `specfact backlog map-fields` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-base-url, --ado-framework, --ado-org, --ado-project, --ado-token, --github-project-id, --github-project-v2-id, --github-type-field-id, --github-type-option, --non-interactive, --provider, --reset; args: - | - | | | `specfact backlog promote` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --item-id, --project-id, --template, --to-status; args: - | - | | -| `specfact backlog refine` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --assignee, --auto-accept-high-confidence, --auto-bundle, --bundle, --check-dor, --custom-field-mapping, --export-to-tmp, --first-comments, --first-issues, --framework, --github-token, --id, --ignore-refined, --import-from-tmp, --iteration, --labels, --last-comments, --last-issues, --limit, --no-ignore-refined, --no-preview, --openspec-comment, --persona, --preview, --release, --repo-name, --repo-owner, --search, --sprint, --state, --tags, --template, --tmp-file, --write; args: - | - | | +| `specfact backlog refine` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --ado-org, --ado-project, --ado-team, --ado-token, --assignee, --auto-accept-high-confidence, --auto-bundle, --bundle, --check-dor, --custom-field-mapping, --export-to-tmp, --first-comments, --first-issues, --framework, --github-token, --id, --ignore-refined, --import-from-tmp, --iteration, --labels, --last-comments, --last-issues, --limit, --no-ignore-refined, --no-preview, --openspec-comment, --persona, --preview, --release, --repo-name, --repo-owner, --search, --sprint, --state, --tags, --template, --tmp-file, --write; args: ADAPTER (required) | - | | | `specfact backlog sync` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --baseline-file, --force-baseline-overwrite, --output-format, --project-id, --template; args: - | - | | | `specfact backlog verify-readiness` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --project-id, --target-items, --template; args: - | - | | | `specfact code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-completion, --show-completion; args: - | analyze, drift, import, repro, validate | | | `specfact code analyze` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | contracts | | | `specfact code analyze contracts` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --bundle, --repo; args: - | - | | | `specfact code drift` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | detect | | -| `specfact code drift detect` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --format, --out, --repo; args: - | - | | -| `specfact code import` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: - | from-bridge, from-code | | +| `specfact code drift detect` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --format, --out, --repo; args: BUNDLE | - | | +| `specfact code import` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | from-bridge, from-code | | | `specfact code import from-bridge` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --adapter, --dry-run, --force, --out-branch, --repo, --report, --write; args: - | - | | -| `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: - | - | | +| `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | - | | | `specfact code repro` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --budget, --crosshair-per-path-timeout, --crosshair-required, --fail-fast, --fix, --out, --repo, --sidecar, --sidecar-bundle, --verbose; args: - | setup | | | `specfact code repro setup` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-crosshair, --repo; args: - | - | | | `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | review | | @@ -63,17 +63,17 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code review review rules init` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | | `specfact code review review rules show` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | | `specfact code review review rules update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | -| `specfact code review review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --preview-fixes, --scope, --score-only, --suppress-noise, --with-mutation; args: - | - | | +| `specfact code review review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --preview-fixes, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | | `specfact code validate` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | sidecar | | | `specfact code validate sidecar` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | init, run | | -| `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | - | | -| `specfact code validate sidecar run` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --no-run-crosshair, --no-run-specmatic, --run-crosshair, --run-specmatic; args: - | - | | +| `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | +| `specfact code validate sidecar run` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --no-run-crosshair, --no-run-specmatic, --run-crosshair, --run-specmatic; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | | `specfact govern` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --install-completion, --show-completion; args: - | enforce, patch | | | `specfact govern enforce` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | -; args: - | sdd, stage | | -| `specfact govern enforce sdd` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --no-interactive, --out, --output-format, --sdd; args: - | - | | +| `specfact govern enforce sdd` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --no-interactive, --out, --output-format, --sdd; args: BUNDLE | - | | | `specfact govern enforce stage` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --preset; args: - | - | | | `specfact govern patch` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | -; args: - | apply | | -| `specfact govern patch apply` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --dry-run, --write, --yes; args: - | - | | +| `specfact govern patch apply` | nold-ai/specfact-govern | `specfact module install nold-ai/specfact-govern` | --dry-run, --write, --yes; args: PATCH_FILE (required) | - | | | `specfact project` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --install-completion, --show-completion; args: - | devops-flow, export, export-roadmap, health-check, import, init-personas, link-backlog, lock, locks, merge, regenerate, resolve-conflict, snapshot, sync, unlock, version | | | `specfact project devops-flow` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --action, --bundle, --no-interactive, --project-name, --repo, --stage, --verbose; args: - | - | | | `specfact project export` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --list-personas, --no-interactive, --out, --output, --output-dir, --persona, --repo, --stdout, --template; args: - | - | | @@ -90,7 +90,7 @@ This file is generated from the current module command trees. Do not edit by han | `specfact project snapshot` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --output, --project-name, --repo; args: - | - | | | `specfact project sync` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | -; args: - | bridge, intelligent, repository | | | `specfact project sync bridge` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --adapter, --add-progress-comment, --ado-base-url, --ado-org, --ado-project, --ado-token, --ado-work-item-type, --all, --backlog-ids, --backlog-ids-file, --bidirectional, --bundle, --change-ids, --code-repo, --ensure-compliance, --export-to-tmp, --external-base-path, --feature, --github-token, --import-from-tmp, --include-archived, --interactive, --interval, --mode, --no-add-progress-comment, --no-gh-cli, --no-include-archived, --no-sanitize, --no-track-code-changes, --no-update-existing, --overwrite, --repo, --repo-name, --repo-owner, --sanitize, --target-repo, --tmp-file, --track-code-changes, --update-existing, --use-gh-cli, --watch; args: - | - | | -| `specfact project sync intelligent` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --code-to-spec, --repo, --spec-to-code, --tests, --watch; args: - | - | | +| `specfact project sync intelligent` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --code-to-spec, --repo, --spec-to-code, --tests, --watch; args: BUNDLE | - | | | `specfact project sync repository` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --confidence, --interval, --repo, --target, --watch; args: - | - | | | `specfact project unlock` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --repo, --section; args: - | - | | | `specfact project version` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | -; args: - | bump, check, set | | @@ -99,11 +99,11 @@ This file is generated from the current module command trees. Do not edit by han | `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 import` | nold-ai/specfact-requirements | `specfact module install nold-ai/specfact-requirements` | --bundle, --format, --from-file, --from-openspec, --from-speckit; args: SOURCE_PATH | - | | | `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: - | - | | +| `specfact spec backward-compat` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | -; args: OLD_SPEC (required), NEW_SPEC (required) | - | | +| `specfact spec generate-tests` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --out, --output; args: SPEC_PATH | - | | | `specfact spec mock` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --examples, --no-interactive, --port, --spec, --strict; args: - | - | | -| `specfact spec validate` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --no-interactive, --previous; args: - | - | | +| `specfact spec validate` | nold-ai/specfact-spec | `specfact module install nold-ai/specfact-spec` | --bundle, --force, --no-interactive, --previous; args: SPEC_PATH | - | | diff --git a/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md b/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md index 1b2db9c1..a290c4c4 100644 --- a/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md +++ b/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md @@ -1,12 +1,34 @@ # Change Validation Report: openspec-01-intent-trace -**Status**: STALE — revalidation required before implementation. +**Status**: VALIDATED — 2026-07-14 (Europe/Berlin) -The previous report (2026-03-05) validated the retired `## Intent Trace` -authoring scope. The change was rescoped on 2026-07-13 to import-first -requirement evidence runtime for native OpenSpec and Spec Kit artifacts (see -`proposal.md`, Rescope section, and core counterpart nold-ai/specfact-cli#350). +## Scope and dependency check -Re-run the change validation workflow against the rescoped proposal and -replace this file with the new report before starting implementation -(tracked as task 1.3 in `tasks.md`). +- The modules change remains a thin command runtime: it delegates native + OpenSpec and Spec Kit imports to core, and does not parse, hash, enrich, or + write upstream artifacts. +- Core counterpart `nold-ai/specfact-cli#350` shipped in merged PR + `nold-ai/specfact-cli#646` on 2026-07-13. Its exported helpers are + `import_openspec_change`, `import_speckit_feature`, and + `validate_requirement_context` with optional profile resolution. +- The module requires core `>=0.52.0,<1.0.0`; the validated local core is + `0.52.2`. +- Core's `unsupported-source-schema` and `unsupported-profile-field` outcomes + remain core-owned and are passed through by the module without fallback + parsing, metadata enrichment, or partial persistence. + +## Change validity evidence + +`openspec validate openspec-01-intent-trace --strict` passed on 2026-07-14. + +The active delta covers the command flags, conservative auto-detection, +sidecar merge behavior, profile delegation, core diagnostic pass-through, and +read-only upstream behavior. No scope change is required beyond the existing +delta updates that record the core compatibility outcomes. + +## Implementation constraints + +1. Add or update scenario-mapped tests before runtime code. +2. Record failing-before and passing-after evidence in `TDD_EVIDENCE.md`. +3. Bump the additive module release and align manifest compatibility metadata. +4. Re-sign the changed module manifest and run the full quality gate sequence. diff --git a/openspec/changes/openspec-01-intent-trace/TDD_EVIDENCE.md b/openspec/changes/openspec-01-intent-trace/TDD_EVIDENCE.md new file mode 100644 index 00000000..889e3493 --- /dev/null +++ b/openspec/changes/openspec-01-intent-trace/TDD_EVIDENCE.md @@ -0,0 +1,78 @@ +# TDD Evidence: openspec-01-intent-trace + +## Failing-before + +- Date: 2026-07-14 (Europe/Berlin) +- Command: + + ```bash + hatch run test tests/unit/specfact_requirements/test_requirements_runtime.py tests/integration/specfact_requirements/test_command_apps.py + ``` + +- Result: failed during collection, as expected before runtime implementation. +- Evidence: `ImportError: cannot import name 'import_native_requirements_to_bundle' from 'specfact_requirements.requirements.runtime'`. + +The new tests cover OpenSpec and Spec Kit import delegation, read-only source +behavior, no partial persistence for core `unsupported-source-schema`, +layered-profile delegation, core gate-count presentation, and CLI explicit and +auto-detected OpenSpec sources. + +## Passing-after + +- Date: 2026-07-14 (Europe/Berlin) +- Command: + + ```bash + hatch run pytest tests/unit/specfact_requirements/test_requirements_runtime.py tests/integration/specfact_requirements/test_command_apps.py -q + ``` + +- Result: `21 passed`. +- The passing suite proves core-delegated OpenSpec and Spec Kit imports, + read-only source handling, fail-closed schema rejection without a sidecar, + layered-profile delegation, core gate-count presentation, and command-level + explicit and auto-detected OpenSpec import behavior. + +## Review-regression evidence + +- Date: 2026-07-14 (Europe/Berlin) +- Command: + + ```bash + hatch run pytest tests/unit/specfact_requirements/test_requirements_runtime.py tests/integration/specfact_requirements/test_command_apps.py -q + ``` + +- Failing-before result: 4 failures. OpenSpec auto-detection treated + `openspec/changes/archive/` as an import candidate; the CLI did not expose an + optional positional source path; and a regular-file source raised a runtime + contract violation instead of a CLI usage error. +- Passing-after result: `21 passed` after archive exclusion, typed optional + source-path parsing, explicit Spec Kit CLI coverage, and generated-command + metadata support were added. + +## Code-review regression evidence + +- Date: 2026-07-14 (Europe/Berlin) +- Command: + + ```bash + hatch run pytest tests/unit/specfact_code_review/tools/test_radon_runner.py -q + ``` + +- Failing-before result: `1 failed, 7 passed`; a Typer command without a + `ctx` parameter still produced `kiss.parameter-count.warning`. +- Passing-after result: `9 passed`; commands are exempt only when their + decorator is owned by a statically identified Typer application, regardless + of whether they accept a context. + +## Final quality gates + +- `hatch run type-check -- packages/specfact-requirements/src/specfact_requirements/requirements packages/specfact-code-review/src/specfact_code_review/tools/radon_runner.py tests/unit/specfact_requirements tests/integration/specfact_requirements/test_command_apps.py tests/unit/specfact_code_review/tools/test_radon_runner.py` — passed with 0 errors. +- `hatch run lint` — passed. +- `hatch run yaml-lint` — passed. +- `hatch run check-bundle-imports` — passed. +- `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --allow-missing-public-key` — passed for all manifests. +- `hatch run contract-test` — passed (28 tests). +- `hatch run smart-test` — passed (884 tests). +- `hatch run test` — passed (884 tests, 2 third-party deprecation warnings). +- `openspec validate openspec-01-intent-trace --strict` — passed. +- `hatch run specfact code review run --enforcement changed --bug-hunt --json --out /private/tmp/specfact-code-review-final.json` — passed with no findings. diff --git a/openspec/changes/openspec-01-intent-trace/design.md b/openspec/changes/openspec-01-intent-trace/design.md index 8e90142e..9e22bd08 100644 --- a/openspec/changes/openspec-01-intent-trace/design.md +++ b/openspec/changes/openspec-01-intent-trace/design.md @@ -48,13 +48,33 @@ error instead of an ImportError traceback. ### D3: Auto-detection stays conservative -**Decision**: When the source path is omitted, detect only conventional -layouts (`openspec/changes/` for OpenSpec; Spec Kit `specs/` feature folders) -relative to the current project root. Ambiguity or absence is a clear error -naming the expected layouts, never a guess. +**Decision**: When the source path is omitted, detect only conventional active +layouts (`openspec/changes/`, excluding its `archive/` directory, for OpenSpec; +Spec Kit `specs/` feature folders) relative to the current project root. +Ambiguity or absence is a clear error naming the expected layouts, never a +guess. **Rationale**: Deterministic behavior over convenience heuristics; misdetected sources would poison evidence. +### D4: Preserve core required-field findings unchanged + +**Decision**: The runtime delegates profile resolution and completeness +evaluation to core, including the evidence-compatible aliases (`id`, `title`, +`acceptance`, and `trace_links`) and `unsupported-profile-field` advisories. +It renders those findings without translating, filtering, or supplementing +them with module-owned metadata. +**Rationale**: The module must remain a thin command adapter. Adding owner, +risk, or exception fields here would make the module an authoring surface and +break the core import-first contract. + +### D5: Preserve core compatibility failures unchanged + +**Decision**: The runtime surfaces `unsupported-source-schema` returned by the +core adapter and does not persist records from that import result. +**Rationale**: Source-profile detection, version policy, fallback parsing, and +partial-import prevention belong to core #350. Reimplementing any of them in +the module would create divergent compatibility behavior. + ## Risks / Trade-offs - **[Risk] Core/module version skew** — flags exist but core helpers are old. diff --git a/openspec/changes/openspec-01-intent-trace/proposal.md b/openspec/changes/openspec-01-intent-trace/proposal.md index da945a1e..8e3458f1 100644 --- a/openspec/changes/openspec-01-intent-trace/proposal.md +++ b/openspec/changes/openspec-01-intent-trace/proposal.md @@ -37,11 +37,18 @@ proposal (nold-ai/specfact-cli#350) for contract details. and Spec Kit `specs/`) relative to the project root. - **EXTEND**: `requirements validate` surfaces the core gate findings (`scenario-unverified`, `stale-import`, `source-missing`, - `ambiguous-mapping`) with profile-driven severity and non-zero exit on - failure; `list`/`coverage` output includes gate-relevant counts. + `ambiguous-mapping`) with profile-driven severity and non-zero exit on failure; + `list`/`coverage` output includes gate-relevant counts. - **EXTEND**: When `--profile` is omitted, the effective profile resolves from the layered configuration shipped by `profile-01-config-layering` instead of a hardcoded `startup` default; an explicit flag always wins. +- **EXTEND**: Preserve the core adapter's evidence-compatible required-field + mapping (`id`, `title`, `acceptance`, and `trace_links`) and surface its + `unsupported-profile-field` advisories unchanged. The module does not add + owner, risk, or exception metadata to imported records. +- **EXTEND**: Surface core `unsupported-source-schema` errors unchanged and do + not persist any partial import when core rejects an untested OpenSpec schema + or a customized Spec Kit template profile. - **UNCHANGED**: `--from-file` remains for generic records; existing sidecar persistence and merge semantics are reused as-is. @@ -60,5 +67,5 @@ proposal (nold-ai/specfact-cli#350) for contract details. - **GitHub Issue**: #168 - **Issue URL**: - **Core Counterpart**: nold-ai/specfact-cli#350 -- **Last Synced Status**: proposed +- **Last Synced Status**: open / Todo (aligned 2026-07-13) - **Sanitized**: false diff --git a/openspec/changes/openspec-01-intent-trace/specs/requirements-module/spec.md b/openspec/changes/openspec-01-intent-trace/specs/requirements-module/spec.md index 48de3a0f..81bb814f 100644 --- a/openspec/changes/openspec-01-intent-trace/specs/requirements-module/spec.md +++ b/openspec/changes/openspec-01-intent-trace/specs/requirements-module/spec.md @@ -23,11 +23,13 @@ logic of its own and SHALL never write into upstream artifact directories. - **THEN** the core adapter normalizes the artifacts into requirement records - **AND** merged records persist to the bundle requirements sidecar. -#### Scenario: Omitted source paths auto-detect conventional layouts +#### Scenario: Omitted source paths auto-detect active conventional layouts -- **GIVEN** a project root containing an `openspec/changes/` directory +- **GIVEN** a project root containing one active OpenSpec change and an + `openspec/changes/archive/` directory - **WHEN** `specfact requirements import --from-openspec` runs without an explicit path -- **THEN** the conventional layout is detected and imported +- **THEN** the active conventional change layout is detected and imported +- **AND** the archive directory is not considered an import source - **AND** a clear error names the expected layouts when detection finds no source. #### Scenario: Validate surfaces gate findings with CI-usable exit codes @@ -38,6 +40,25 @@ logic of its own and SHALL never write into upstream artifact directories. - **THEN** the report lists each gate finding with its category and affected requirement IDs - **AND** the command exits non-zero when the profile treats any finding as an error. +#### Scenario: Runtime preserves core required-field advisories + +- **GIVEN** core validation returns an `unsupported-profile-field` advisory + for a profile field not represented by `RequirementInput` +- **WHEN** `specfact requirements validate` renders the validation report +- **THEN** the advisory is present in the machine-readable and human-readable + output unchanged +- **AND** the module does not add owner, risk, or exception metadata to the + imported record. + +#### Scenario: Runtime blocks unsupported source profiles + +- **GIVEN** an OpenSpec schema or Spec Kit template profile rejected by the + core adapter with `unsupported-source-schema` +- **WHEN** an import command delegates to core +- **THEN** the command surfaces that diagnostic unchanged +- **AND** it does not create or persist partial requirement records +- **AND** it does not attempt version detection or fallback parsing. + #### Scenario: Runtime never writes upstream - **GIVEN** import and validation runs against OpenSpec and Spec Kit sources diff --git a/openspec/changes/openspec-01-intent-trace/tasks.md b/openspec/changes/openspec-01-intent-trace/tasks.md index 839377ca..f46ff159 100644 --- a/openspec/changes/openspec-01-intent-trace/tasks.md +++ b/openspec/changes/openspec-01-intent-trace/tasks.md @@ -17,15 +17,17 @@ evidence in `TDD_EVIDENCE.md`. - [ ] 2.1 Finalize the `requirements-module` spec delta and cross-check scenario completeness. - [ ] 2.2 Add tests: `--from-openspec`/`--from-speckit` import against fixtures, auto-detection success and clear-error cases, gate findings surfaced in `validate` with non-zero exit, and upstream directories untouched after runs. -- [ ] 2.2b Add tests: omitted `--profile` resolves the effective profile from layered configuration instead of a hardcoded `startup` default; explicit `--profile` overrides. +- [ ] 2.2b Add tests: omitted `--profile` resolves the effective profile from layered configuration instead of a hardcoded `startup` default; explicit `--profile` overrides; core's four supported aliases and `unsupported-profile-field` advisories pass through unchanged. +- [ ] 2.2c Add tests: core `unsupported-source-schema` diagnostics surface unchanged and produce no partial sidecar persistence. - [ ] 2.3 Run targeted tests, capture failing-first output in `TDD_EVIDENCE.md`. ## 3. Implementation -- [ ] 3.1 Extend `packages/specfact-requirements/.../commands.py` import command with `--from-openspec [PATH]` and `--from-speckit [PATH]` options. -- [ ] 3.2 Extend `runtime.py` with thin delegation to the new core normalizers via the existing `_load_requirements_module` pattern, including `RequirementsCoreUnavailableError` messaging. -- [ ] 3.3 Surface gate findings in `validate` output and gate-relevant counts in `list`/`coverage`. -- [ ] 3.3b Change the `validate` command's profile default from hardcoded `startup` to core layered-config resolution when the flag is omitted (delegated to the core helper; no config parsing in the module). +- [x] 3.1 Extend `packages/specfact-requirements/.../commands.py` import command with `--from-openspec [PATH]` and `--from-speckit [PATH]` options. +- [x] 3.2 Extend `runtime.py` with thin delegation to the new core normalizers via the existing `_load_requirements_module` pattern, including `RequirementsCoreUnavailableError` messaging. +- [x] 3.3 Surface gate findings in `validate` output and gate-relevant counts in `list`/`coverage`. +- [x] 3.3b Change the `validate` command's profile default from hardcoded `startup` to core layered-config resolution when the flag is omitted, and render core required-field advisories unchanged (delegated to the core helper; no config parsing or metadata enrichment in the module). +- [x] 3.3c Surface core `unsupported-source-schema` results without fallback parsing or partial persistence. - [ ] 3.4 Contract decorators (`@beartype`, `@require`, `@ensure`) on all new public APIs. ## 4. Validation and documentation diff --git a/packages/specfact-code-review/module-package.yaml b/packages/specfact-code-review/module-package.yaml index 0e62afec..afdf906d 100644 --- a/packages/specfact-code-review/module-package.yaml +++ b/packages/specfact-code-review/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-code-review -version: 0.47.48 +version: 0.47.51 commands: - code tier: official @@ -23,5 +23,5 @@ description: Official SpecFact code review bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:666caa2524a6c569dbecc9450ef690035960de5232e6547904edd813bede2745 - signature: Yn9yF3FxljA4nzTnN8XtWph9v/K6jeg8mAkeBN/yDjx5sCeyKDQ9DWCHJhLtMeNbfW1cQESdAlC+P2KFx1fgCw== + checksum: sha256:f85ae00bc406952621b590367370ef7a9d28acbc26703befa5b1b1a76a6ef516 + signature: fJJEEZliDUcVlOmmjMLAqrYAjHz+r1jVLU8JB09SDeKTVM0yrilkWCqnGpKKMAwat4dw/s0ispT+brZogMqoAQ== diff --git a/packages/specfact-code-review/src/specfact_code_review/tools/radon_runner.py b/packages/specfact-code-review/src/specfact_code_review/tools/radon_runner.py index 7955dba4..2ac30b6d 100644 --- a/packages/specfact-code-review/src/specfact_code_review/tools/radon_runner.py +++ b/packages/specfact-code-review/src/specfact_code_review/tools/radon_runner.py @@ -106,13 +106,14 @@ def _kiss_metric_findings(file_path: Path) -> list[ReviewFinding]: except (OSError, SyntaxError) as exc: return _tool_error(file_path, f"Unable to parse source for KISS metrics: {exc}") + typer_command_targets = _typer_command_targets(tree) findings: list[ReviewFinding] = [] for function_node in ast.walk(tree): if not isinstance(function_node, ast.FunctionDef | ast.AsyncFunctionDef): continue findings.extend(_kiss_loc_findings(function_node, file_path)) findings.extend(_kiss_nesting_findings(function_node, file_path)) - findings.extend(_kiss_parameter_findings(function_node, file_path)) + findings.extend(_kiss_parameter_findings(function_node, file_path, typer_command_targets)) return findings @@ -165,14 +166,10 @@ def _kiss_nesting_findings( return findings -def _typer_cli_entrypoint_exempt(function_node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: Path) -> bool: +def _typer_cli_entrypoint_exempt( + function_node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: Path, typer_command_targets: set[str] +) -> bool: """Typer command callbacks legitimately take many injected options; skip parameter-count KISS on them.""" - args0 = function_node.args.args - if not args0: - return False - first = args0[0] - if first.arg != "ctx": - return False normalized = str(file_path).replace("\\", "/") # Stable path suffix: matches in-repo and user-scoped installs (~/.specfact/modules/.../src/...). # Typer CLI handler `run(ctx: Context, ...)` in review.commands injects many option parameters by @@ -180,16 +177,7 @@ def _typer_cli_entrypoint_exempt(function_node: ast.FunctionDef | ast.AsyncFunct # elsewhere still get complexity checks. if function_node.name == "run" and normalized.endswith("specfact_code_review/review/commands.py"): return True - if not _has_typer_command_decorator(function_node): - return False - ann = first.annotation - if ann is None: - return False - try: - rendered = ast.unparse(ann) - except AttributeError: - return False - return rendered.endswith("Context") + return _has_typer_command_decorator(function_node, typer_command_targets) def _decorator_name_parts(decorator: ast.expr) -> tuple[str, ...]: @@ -202,21 +190,82 @@ def _decorator_name_parts(decorator: ast.expr) -> tuple[str, ...]: return () -def _has_typer_command_decorator(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: +def _typer_command_targets(tree: ast.Module) -> set[str]: + """Return names that own Typer command or callback decorators in a module.""" + typer_module_names = _typer_module_names(tree) + typer_constructor_names = _typer_constructor_names(tree) + return typer_module_names | _typer_application_names(tree, typer_module_names, typer_constructor_names) + + +def _typer_module_names(tree: ast.Module) -> set[str]: + names: set[str] = set() + for node in tree.body: + if not isinstance(node, ast.Import): + continue + for alias in node.names: + if alias.name == "typer": + names.add(alias.asname or alias.name) + return names + + +def _typer_constructor_names(tree: ast.Module) -> set[str]: + names: set[str] = set() + for node in tree.body: + if not isinstance(node, ast.ImportFrom) or node.module != "typer": + continue + for alias in node.names: + if alias.name == "Typer": + names.add(alias.asname or alias.name) + return names + + +def _typer_application_names( + tree: ast.Module, typer_module_names: set[str], typer_constructor_names: set[str] +) -> set[str]: + names: set[str] = set() + for node in tree.body: + if isinstance(node, ast.Assign | ast.AnnAssign) and _is_typer_constructor( + node.value, typer_module_names, typer_constructor_names + ): + names.update(_assignment_target_names(node)) + return names + + +def _is_typer_constructor( + value: ast.expr | None, typer_module_names: set[str], typer_constructor_names: set[str] +) -> bool: + if not isinstance(value, ast.Call): + return False + if isinstance(value.func, ast.Name): + return value.func.id in typer_constructor_names + return ( + isinstance(value.func, ast.Attribute) + and isinstance(value.func.value, ast.Name) + and value.func.value.id in typer_module_names + and value.func.attr == "Typer" + ) + + +def _assignment_target_names(node: ast.Assign | ast.AnnAssign) -> set[str]: + targets = node.targets if isinstance(node, ast.Assign) else [node.target] + return {target.id for target in targets if isinstance(target, ast.Name)} + + +def _has_typer_command_decorator( + function_node: ast.FunctionDef | ast.AsyncFunctionDef, typer_command_targets: set[str] +) -> bool: for decorator in function_node.decorator_list: parts = _decorator_name_parts(decorator) - if parts == ("command",) or parts[-1:] == ("command",): - return True - if parts[-1:] == ("callback",): + if len(parts) == 2 and parts[0] in typer_command_targets and parts[1] in {"command", "callback"}: return True return False def _kiss_parameter_findings( - function_node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: Path + function_node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: Path, typer_command_targets: set[str] ) -> list[ReviewFinding]: findings: list[ReviewFinding] = [] - if _typer_cli_entrypoint_exempt(function_node, file_path): + if _typer_cli_entrypoint_exempt(function_node, file_path, typer_command_targets): return findings parameter_count = len(function_node.args.posonlyargs) parameter_count += len(function_node.args.args) diff --git a/packages/specfact-requirements/module-package.yaml b/packages/specfact-requirements/module-package.yaml index e465cb26..a5ab3c8c 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.5 +version: 0.2.3 commands: - requirements tier: official @@ -14,10 +14,10 @@ pip_dependencies: - pydantic - pyyaml - typer -core_compatibility: '>=0.50.0,<1.0.0' +core_compatibility: '>=0.52.0,<1.0.0' description: Official SpecFact requirements evidence runtime bundle package. category: project bundle_group_command: requirements integrity: - checksum: sha256:d200aaecff7760ee660dd984d70d417b56960879385ebd745ddf48f08cee1b0d - signature: 3cXSjTAnhQHUcvDsM4pYwp8OxJ/gAvNtZD3H0zCMqOyuHW5axSeCJcpGDqjno1YLAq+v4B1PL5HYuJkAUeZQAQ== + checksum: sha256:02ae009efbaba529ba0b4abc14a0ca11c0d08d6451dff094236ce3e850f046a7 + signature: 2WfMBL5+ISTzij7cpqX25K2blP7XXG52jcWc43xE8lPEJE8AKnMsYzZfArd75VVPjTS1kPqzwKxmGlGwodpaDg== diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py index 2064958f..a4af0297 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/commands.py @@ -9,13 +9,19 @@ import typer from beartype import beartype +from click import Context from icontract import ensure, require from specfact_requirements.requirements.runtime import ( + auto_detect_openspec_change, + auto_detect_speckit_feature, + import_native_requirements_to_bundle, import_requirements_file_to_bundle, + import_result_has_errors, inspect_requirements_bundle_coverage, is_requirement_context_profile_supported, list_requirements_with_coverage, + requirements_gate_finding_counts, validate_requirements_bundle, ) @@ -49,27 +55,84 @@ def _emit_payload(payload: dict[str, Any], output_format: OutputFormat) -> None: typer.echo(f"{key}: {value}") -@app.command("import", help="Import local requirement records into a project bundle.") +def _selected_import_source(from_file: Path | None, from_openspec: bool, from_speckit: bool) -> str: + selected = [ + name + for name, enabled in ( + ("file", from_file is not None), + ("openspec", from_openspec), + ("speckit", from_speckit), + ) + if enabled + ] + if len(selected) != 1: + raise typer.BadParameter("choose exactly one of --from-file, --from-openspec, or --from-speckit") + return selected[0] + + +@app.command("import", help="Import local, OpenSpec, or Spec Kit requirement evidence 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." - ), - ], + ctx: Context, bundle: Annotated[ Path, typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True, writable=True), ], + source_path: Annotated[ + Path | None, + typer.Argument( + exists=True, + file_okay=False, + dir_okay=True, + readable=True, + help="Optional OpenSpec change or Spec Kit feature directory.", + ), + ] = None, + from_file: Annotated[ + Path | None, + typer.Option( + "--from-file", exists=True, file_okay=True, dir_okay=False, readable=True, help="JSON/YAML records." + ), + ] = None, + from_openspec: Annotated[ + bool, + typer.Option( + "--from-openspec", help="Import one OpenSpec change; an optional positional path overrides auto-detection." + ), + ] = False, + from_speckit: Annotated[ + bool, + typer.Option( + "--from-speckit", help="Import one Spec Kit feature; an optional positional path overrides auto-detection." + ), + ] = False, 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) + """Import one source of requirement evidence into a project bundle.""" + del ctx # Typer injects the Click context so the command remains compatible with context-aware tooling. + source_name = _selected_import_source(from_file, from_openspec, from_speckit) + if source_name == "file": + if source_path is not None: + raise typer.BadParameter("--from-file does not accept a positional source path") + if from_file is None: + raise typer.BadParameter("--from-file requires a path") + result = import_requirements_file_to_bundle(from_file, bundle) + else: + source_dir = source_path + if source_dir is None: + source_dir = ( + auto_detect_openspec_change(Path.cwd()) + if source_name == "openspec" + else auto_detect_speckit_feature(Path.cwd()) + ) + result = import_native_requirements_to_bundle( + source_name, + source_dir, + bundle, + ) _emit_payload( { "imported": len(result.requirements), @@ -77,12 +140,17 @@ def import_command( }, output_format, ) + if import_result_has_errors(result): + raise typer.Exit(1) @app.command("validate", help="Validate requirement context evidence usefulness.") @beartype @require(lambda bundle: bundle.is_dir(), "bundle must exist") -@require(is_requirement_context_profile_supported, "profile must be a known requirement context profile") +@require( + lambda profile: profile is None or is_requirement_context_profile_supported(profile), + "profile must be a known requirement context profile when provided", +) @require(_format_supported, "output format must be supported") @ensure(lambda result: result is None) def validate_command( @@ -90,7 +158,9 @@ def validate_command( Path, typer.Option("--bundle", exists=True, file_okay=False, dir_okay=True, readable=True), ], - profile: Annotated[str, typer.Option("--profile", help="Validation profile.")] = "startup", + profile: Annotated[ + str | None, typer.Option("--profile", help="Validation profile; omit to use layered configuration.") + ] = None, output_format: Annotated[OutputFormat, typer.Option("--format", help="Output format.")] = OutputFormat.TEXT, ) -> None: """Validate requirement context evidence usefulness.""" @@ -131,4 +201,6 @@ def coverage_command( ) -> None: """Inspect requirement context coverage.""" coverage = inspect_requirements_bundle_coverage(bundle) - _emit_payload(coverage.model_dump(mode="json"), output_format) + payload = coverage.model_dump(mode="json") + payload["gate_finding_counts"] = requirements_gate_finding_counts(bundle) + _emit_payload(payload, output_format) diff --git a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py index 0d140d55..0cffe6e3 100644 --- a/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py +++ b/packages/specfact-requirements/src/specfact_requirements/requirements/runtime.py @@ -20,6 +20,11 @@ _REQUIREMENTS_INPUTS_DIR = "requirements" _REQUIREMENTS_MODELS_MODULE = "specfact_cli.models.requirements" _REQUIREMENTS_CONTEXT_MODULE = "specfact_cli.requirements.context" +_REQUIREMENTS_PUBLIC_MODULE = "specfact_cli.requirements" +_NATIVE_IMPORT_HELPERS = { + "openspec": "import_openspec_change", + "speckit": "import_speckit_feature", +} class RequirementsCoreUnavailableError(RuntimeError): @@ -32,7 +37,7 @@ def _load_requirements_module(module_name: str, purpose: str) -> Any: except ImportError as exc: msg = ( f"specfact-requirements requires the paired specfact-cli {purpose} " - "from core change requirements-02-module-commands" + "from core change #350 (specfact-cli >=0.52.0)" ) raise RequirementsCoreUnavailableError(msg) from exc @@ -49,6 +54,30 @@ def _has_attributes(result: Any, *attributes: str) -> bool: return all(hasattr(result, attribute) for attribute in attributes) +def _diagnostic_is_error(diagnostic: Any) -> bool: + return str(getattr(diagnostic, "severity", "")) == "error" + + +@beartype +@require( + lambda import_result: all(hasattr(import_result, attribute) for attribute in ("requirements", "diagnostics")), + "import_result must expose requirements and diagnostics", +) +@ensure(lambda result: isinstance(result, bool)) +def import_result_has_errors(import_result: Any) -> bool: + """Return whether a core import result contains an error diagnostic.""" + return any(_diagnostic_is_error(diagnostic) for diagnostic in import_result.diagnostics) + + +def _core_import_helper(helper_name: str) -> Any: + core_requirements = _load_requirements_module(_REQUIREMENTS_PUBLIC_MODULE, "native requirement import helpers") + helper = getattr(core_requirements, helper_name, None) + if callable(helper): + return helper + msg = f"specfact-requirements requires specfact-cli >=0.52.0; missing core helper '{helper_name}' from change #350" + raise RequirementsCoreUnavailableError(msg) + + def _profile_aliases(profiles: frozenset[str]) -> frozenset[str]: aliases = set(profiles) aliases.update(profile.replace("_", "-") for profile in profiles) @@ -129,6 +158,15 @@ def _write_requirements_sidecar(bundle_dir: Path, records: Sequence[Any]) -> Non temporary_sidecar.unlink() +def _persist_imported_requirements(bundle_dir: Path, imported: Sequence[Any]) -> None: + context_helpers = _load_requirements_module(_REQUIREMENTS_CONTEXT_MODULE, "requirements context helpers") + bundle = _load_bundle_with_requirements(bundle_dir) + merged = merge_requirement_inputs(context_helpers.load_requirements_from_bundle(bundle), imported) + context_helpers.attach_requirements_to_bundle(bundle, merged) + save_project_bundle(bundle, bundle_dir, atomic=True) + _write_requirements_sidecar(bundle_dir, merged) + + @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") @@ -175,23 +213,91 @@ def import_requirements_file_to_bundle(source_file: Path, bundle_dir: Path) -> A records = load_requirement_records(source_file) 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(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) + _persist_imported_requirements(bundle_dir, result.requirements) return result @beartype +@require(lambda source_kind: source_kind in _NATIVE_IMPORT_HELPERS, "source_kind must be openspec or speckit") +@require(lambda source_dir: source_dir.is_dir(), "source_dir must exist") @require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") -@require(is_requirement_context_profile_supported, "profile must be a known requirement context profile") +@ensure(lambda result: _has_attributes(result, "requirements", "diagnostics")) +def import_native_requirements_to_bundle(source_kind: str, source_dir: Path, bundle_dir: Path) -> Any: + """Delegate a native source import to core and persist only valid records.""" + result = _core_import_helper(_NATIVE_IMPORT_HELPERS[source_kind])(source_dir) + if not import_result_has_errors(result): + _persist_imported_requirements(bundle_dir, result.requirements) + return result + + +def _sole_source(candidates: list[Path], expected_layout: str) -> Path: + if len(candidates) == 1: + return candidates[0] + if not candidates: + raise ValueError(f"No import source found; expected {expected_layout}.") + names = ", ".join(candidate.name for candidate in candidates) + raise ValueError(f"Multiple import sources found ({names}); pass an explicit path instead of {expected_layout}.") + + +@beartype +@require(lambda project_root: project_root.is_dir(), "project_root must exist") +@ensure(lambda result: result.is_dir()) +def auto_detect_openspec_change(project_root: Path) -> Path: + """Return the single conventional OpenSpec change source below a project root.""" + changes_dir = project_root / "openspec" / "changes" + candidates = ( + [path for path in sorted(changes_dir.iterdir()) if path.is_dir() and path.name != "archive"] + if changes_dir.is_dir() + else [] + ) + return _sole_source(candidates, "openspec/changes/") + + +@beartype +@require(lambda project_root: project_root.is_dir(), "project_root must exist") +@ensure(lambda result: result.is_dir()) +def auto_detect_speckit_feature(project_root: Path) -> Path: + """Return the single conventional Spec Kit feature source below a project root.""" + specs_dir = project_root / "specs" + candidates = ( + [path for path in sorted(specs_dir.iterdir()) if path.is_dir() and (path / "spec.md").is_file()] + if specs_dir.is_dir() + else [] + ) + return _sole_source(candidates, "specs//") + + +@beartype +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@require( + lambda profile: profile is None or is_requirement_context_profile_supported(profile), + "profile must be a known requirement context profile when provided", +) @ensure(lambda result: isinstance(result, ValidationReport)) -def validate_requirements_bundle(bundle_dir: Path, *, profile: str = "startup") -> ValidationReport: +def validate_requirements_bundle(bundle_dir: Path, *, profile: str | None = None) -> ValidationReport: """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=normalize_requirement_context_profile(profile)) + normalized_profile = normalize_requirement_context_profile(profile) if profile is not None else None + return context_helpers.validate_requirement_context(bundle, profile=normalized_profile, project_root=Path.cwd()) + + +@beartype +@require(lambda bundle_dir: bundle_dir.is_dir(), "bundle_dir must exist") +@require( + lambda profile: profile is None or is_requirement_context_profile_supported(profile), + "profile must be a known requirement context profile when provided", +) +@ensure(lambda result: all(isinstance(code, str) and isinstance(count, int) for code, count in result.items())) +def requirements_gate_finding_counts(bundle_dir: Path, *, profile: str | None = None) -> dict[str, int]: + """Count core validation findings without reimplementing their gate logic.""" + report = validate_requirements_bundle(bundle_dir, profile=profile) + counts: dict[str, int] = {} + for violation in report.violations: + code = violation.get("code") + if isinstance(code, str): + counts[code] = counts.get(code, 0) + 1 + return counts @beartype @@ -225,4 +331,5 @@ def list_requirements_with_coverage(bundle_dir: Path, *, show_coverage: bool = F } if show_coverage: payload["coverage"] = context_helpers.inspect_requirement_context_coverage(requirements).model_dump(mode="json") + payload["gate_finding_counts"] = requirements_gate_finding_counts(bundle_dir) return payload diff --git a/registry/index.json b/registry/index.json index 9844978d..9f3fb0f9 100644 --- a/registry/index.json +++ b/registry/index.json @@ -78,9 +78,9 @@ }, { "id": "nold-ai/specfact-code-review", - "latest_version": "0.47.48", - "download_url": "modules/specfact-code-review-0.47.48.tar.gz", - "checksum_sha256": "dc41ee6a479bbc07f74670ff99f995ae75246da0ad94678957b8efbde1717501", + "latest_version": "0.47.51", + "download_url": "modules/specfact-code-review-0.47.51.tar.gz", + "checksum_sha256": "ee0b2a366293ecf7f3d08331287397322cea35cda5cbd3af7b9df01bd3c00ab1", "core_compatibility": ">=0.44.0,<1.0.0", "tier": "official", "publisher": { @@ -94,9 +94,9 @@ }, { "id": "nold-ai/specfact-requirements", - "latest_version": "0.1.5", - "download_url": "modules/specfact-requirements-0.1.5.tar.gz", - "checksum_sha256": "fd3f684be7d8ba76c16296c00e44f7ab05dbddf00b90bb716d5b9882bbc6745c", + "latest_version": "0.2.3", + "download_url": "modules/specfact-requirements-0.2.3.tar.gz", + "checksum_sha256": "98e343595ef4b2a63e8f5375a7f546774c3c3aee918a0f24370a52ba3369e4c6", "tier": "official", "publisher": { "name": "nold-ai", @@ -105,7 +105,8 @@ "bundle_dependencies": [ "nold-ai/specfact-project" ], - "description": "Official SpecFact requirements evidence runtime bundle package." + "description": "Official SpecFact requirements evidence runtime bundle package.", + "core_compatibility": ">=0.52.0,<1.0.0" } ] } diff --git a/registry/modules/specfact-code-review-0.47.51.tar.gz b/registry/modules/specfact-code-review-0.47.51.tar.gz new file mode 100644 index 00000000..c7c51b38 Binary files /dev/null and b/registry/modules/specfact-code-review-0.47.51.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.47.51.tar.gz.sha256 b/registry/modules/specfact-code-review-0.47.51.tar.gz.sha256 new file mode 100644 index 00000000..6ffe8932 --- /dev/null +++ b/registry/modules/specfact-code-review-0.47.51.tar.gz.sha256 @@ -0,0 +1 @@ +ee0b2a366293ecf7f3d08331287397322cea35cda5cbd3af7b9df01bd3c00ab1 diff --git a/registry/modules/specfact-requirements-0.2.3.tar.gz b/registry/modules/specfact-requirements-0.2.3.tar.gz new file mode 100644 index 00000000..bf100e18 Binary files /dev/null and b/registry/modules/specfact-requirements-0.2.3.tar.gz differ diff --git a/registry/modules/specfact-requirements-0.2.3.tar.gz.sha256 b/registry/modules/specfact-requirements-0.2.3.tar.gz.sha256 new file mode 100644 index 00000000..8a8eceac --- /dev/null +++ b/registry/modules/specfact-requirements-0.2.3.tar.gz.sha256 @@ -0,0 +1 @@ +98e343595ef4b2a63e8f5375a7f546774c3c3aee918a0f24370a52ba3369e4c6 diff --git a/scripts/generate-command-overview.py b/scripts/generate-command-overview.py index e8419bde..65d867fc 100644 --- a/scripts/generate-command-overview.py +++ b/scripts/generate-command-overview.py @@ -154,8 +154,8 @@ def validate_official_mount_inventory() -> None: def _command_options(command: click.Command) -> list[str]: options: set[str] = set() for param in command.params: - if hasattr(param, "opts"): - secondary_opts = getattr(param, "secondary_opts", ()) + if isinstance(param, click.Option): + secondary_opts = param.secondary_opts options.update(opt for opt in [*param.opts, *secondary_opts] if opt.startswith("--")) return sorted(options) @@ -163,7 +163,7 @@ def _command_options(command: click.Command) -> list[str]: def _command_arguments(command: click.Command) -> list[dict[str, Any]]: arguments: list[dict[str, Any]] = [] for param in command.params: - if not hasattr(param, "opts") and hasattr(param, "human_readable_name"): + if isinstance(param, click.Argument): arguments.append( { "name": param.human_readable_name, @@ -208,10 +208,10 @@ def _has_bare_business_parameters(command: click.Command) -> bool: "--show-completion", } for param in command.params: - if not hasattr(param, "opts") and hasattr(param, "human_readable_name"): + if isinstance(param, click.Argument): return True - if hasattr(param, "opts"): - opts = set(param.opts) | set(getattr(param, "secondary_opts", ())) + if isinstance(param, click.Option): + opts = set(param.opts) | set(param.secondary_opts) if opts and opts.isdisjoint(ignored_options): return True return False diff --git a/tests/integration/specfact_requirements/test_command_apps.py b/tests/integration/specfact_requirements/test_command_apps.py index 320e5928..4eefe7af 100644 --- a/tests/integration/specfact_requirements/test_command_apps.py +++ b/tests/integration/specfact_requirements/test_command_apps.py @@ -49,6 +49,53 @@ def _source_file(tmp_path: Path) -> Path: return source +def _openspec_change(project_root: Path) -> Path: + change_dir = project_root / "openspec" / "changes" / "widget-evidence" + spec_path = change_dir / "specs" / "widgets" / "spec.md" + spec_path.parent.mkdir(parents=True) + spec_path.write_text( + """## ADDED Requirements + +### Requirement: Widget rendering + +The system SHALL render a widget. + +#### Scenario: Render a valid widget + +- **GIVEN** a valid widget request +- **WHEN** rendering runs +- **THEN** the widget is returned +""", + encoding="utf-8", + ) + return change_dir + + +def _speckit_feature(project_root: Path) -> Path: + feature_dir = project_root / "specs" / "001-widget-rendering" + feature_dir.mkdir(parents=True) + (feature_dir / "spec.md").write_text( + """# Feature Specification: Widget rendering + +## User Scenarios & Testing + +### User Story 1 - Render widgets (Priority: P1) + +As a user, I want widgets rendered so that I can see them. + +**Acceptance Scenarios**: + +1. **Given** a valid widget request, **When** rendering runs, **Then** the widget is returned + +## Requirements + +- **FR-001**: System MUST render a widget +""", + encoding="utf-8", + ) + return feature_dir + + @pytest.mark.integration def test_command_module_exposes_typer_app() -> None: assert app is not None @@ -119,3 +166,86 @@ def test_requirements_help_exposes_no_author_command() -> None: assert "validate" in result.output assert "coverage" in result.output assert "author" not in result.output + + +@pytest.mark.integration +def test_requirements_import_help_exposes_optional_native_source_path() -> None: + result = runner.invoke(app, ["import", "--help"]) + + assert result.exit_code == 0 + assert "[SOURCE_PATH]" in result.output + + +@pytest.mark.integration +def test_requirements_import_openspec_accepts_explicit_and_auto_detected_sources( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + explicit_bundle = _bundle_dir(tmp_path / "explicit") + + explicit_result = runner.invoke( + app, + ["import", "--from-openspec", str(change_dir), "--bundle", str(explicit_bundle), "--format", "json"], + ) + + assert explicit_result.exit_code == 0, explicit_result.output + assert json.loads(explicit_result.output)["imported"] == 1 + + auto_bundle = _bundle_dir(tmp_path / "auto") + (project_root / "openspec" / "changes" / "archive" / "2026-07-14-widget-evidence").mkdir(parents=True) + monkeypatch.chdir(project_root) + auto_result = runner.invoke( + app, + ["import", "--from-openspec", "--bundle", str(auto_bundle), "--format", "json"], + ) + + assert auto_result.exit_code == 0, auto_result.output + assert json.loads(auto_result.output)["imported"] == 1 + + +@pytest.mark.integration +def test_requirements_import_speckit_accepts_explicit_source(tmp_path: Path) -> None: + project_root = tmp_path / "project" + feature_dir = _speckit_feature(project_root) + bundle_dir = _bundle_dir(tmp_path) + + result = runner.invoke( + app, + ["import", "--from-speckit", str(feature_dir), "--bundle", str(bundle_dir), "--format", "json"], + ) + + assert result.exit_code == 0, result.output + assert json.loads(result.output)["imported"] == 1 + + +@pytest.mark.integration +def test_requirements_import_rejects_non_directory_native_source_path(tmp_path: Path) -> None: + bundle_dir = _bundle_dir(tmp_path) + source_file = tmp_path / "source.md" + source_file.write_text("not a source directory\n", encoding="utf-8") + + result = runner.invoke( + app, + ["import", "--from-openspec", str(source_file), "--bundle", str(bundle_dir), "--format", "json"], + ) + + assert result.exit_code == 2 + assert "Invalid value" in result.output + + +@pytest.mark.integration +def test_requirements_import_surfaces_core_schema_rejection_without_persistence(tmp_path: Path) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + (project_root / "openspec" / "config.yaml").write_text("schema: company-custom\n", encoding="utf-8") + bundle_dir = _bundle_dir(tmp_path) + + result = runner.invoke( + app, + ["import", "--from-openspec", str(change_dir), "--bundle", str(bundle_dir), "--format", "json"], + ) + + assert result.exit_code == 1 + assert json.loads(result.output)["diagnostics"][0]["code"] == "unsupported-source-schema" + assert not (bundle_dir / "reports" / "requirements" / "inputs.yaml").exists() diff --git a/tests/unit/docs/test_bundle_overview_cli_examples.py b/tests/unit/docs/test_bundle_overview_cli_examples.py index aa3ce5c5..63b8f878 100644 --- a/tests/unit/docs/test_bundle_overview_cli_examples.py +++ b/tests/unit/docs/test_bundle_overview_cli_examples.py @@ -60,6 +60,22 @@ "import", "--help", ], + ( + "specfact requirements import --from-openspec openspec/changes/widget-evidence " + "--bundle .specfact/projects/shop --format json" + ): [ + "requirements", + "import", + "--help", + ], + ( + "specfact requirements import --from-speckit specs/001-widget-rendering " + "--bundle .specfact/projects/shop --format json" + ): [ + "requirements", + "import", + "--help", + ], "specfact requirements list --bundle .specfact/projects/shop --show-coverage --format json": [ "requirements", "list", diff --git a/tests/unit/docs/test_llms_overview_freshness.py b/tests/unit/docs/test_llms_overview_freshness.py index c04585b5..5a3e0784 100644 --- a/tests/unit/docs/test_llms_overview_freshness.py +++ b/tests/unit/docs/test_llms_overview_freshness.py @@ -13,6 +13,7 @@ import sys from pathlib import Path +import click import pytest from tests.unit._script_test_utils import load_module_from_path @@ -52,6 +53,15 @@ def test_llms_and_command_overview_are_current() -> None: ) +def test_command_overview_records_optional_click_arguments() -> None: + generator = load_module_from_path("generate_command_overview_arguments", GENERATOR) + command = click.Command("import", params=[click.Argument(["source_path"], required=False)]) + + assert generator._command_arguments(command) == [ # pylint: disable=protected-access + {"name": "SOURCE_PATH", "required": False, "nargs": 1} + ] + + def test_command_overview_rejects_unrepresented_official_inventory(tmp_path: Path, monkeypatch) -> None: generator = load_module_from_path("generate_command_overview_inventory", GENERATOR) manifest = tmp_path / "packages" / "specfact-example" / "module-package.yaml" diff --git a/tests/unit/specfact_code_review/tools/test_radon_runner.py b/tests/unit/specfact_code_review/tools/test_radon_runner.py index e3c32e56..3c8affe1 100644 --- a/tests/unit/specfact_code_review/tools/test_radon_runner.py +++ b/tests/unit/specfact_code_review/tools/test_radon_runner.py @@ -5,19 +5,34 @@ from pathlib import Path from unittest.mock import Mock +import pytest from pytest import MonkeyPatch from specfact_code_review.tools.radon_runner import run_radon from tests.unit.specfact_code_review.tools.helpers import assert_tool_run, completed_process, create_noisy_file +def _parameter_count_rules(tmp_path: Path, monkeypatch: MonkeyPatch, source: str) -> set[str]: + file_path = tmp_path / "commands.py" + file_path.write_text(source, encoding="utf-8") + monkeypatch.setattr( + subprocess, + "run", + Mock(return_value=completed_process("radon", stdout=json.dumps({str(file_path): []}))), + ) + return {finding.rule for finding in run_radon([file_path])} + + def test_run_radon_returns_empty_when_only_non_python_paths(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: manifest = tmp_path / "module-package.yaml" manifest.write_text("name: example\n", encoding="utf-8") run_mock = Mock() monkeypatch.setattr(subprocess, "run", run_mock) - assert run_radon([manifest]) == [] + result = run_radon([manifest]) + + assert isinstance(result, list) + assert not result run_mock.assert_not_called() @@ -60,7 +75,8 @@ def test_run_radon_returns_no_findings_for_complexity_twelve_or_below(tmp_path: findings = run_radon([file_path]) - assert findings == [] + assert isinstance(findings, list) + assert not findings def test_run_radon_returns_tool_error_on_parse_error(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: @@ -109,44 +125,45 @@ def test_run_radon_uses_dedicated_tool_identifier_for_kiss_findings(tmp_path: Pa assert {finding.tool for finding in kiss_findings} == {"radon-kiss"} -def test_run_radon_requires_typer_decorator_for_context_parameter_exemption( - tmp_path: Path, monkeypatch: MonkeyPatch -) -> None: - file_path = tmp_path / "commands.py" - file_path.write_text( - """ +@pytest.mark.parametrize( + ("source", "expects_parameter_count_warning"), + [ + ( + """ def callback(ctx: typer.Context, a: str, b: str, c: str, d: str, e: str) -> None: return None """, - encoding="utf-8", - ) - monkeypatch.setattr( - subprocess, - "run", - Mock(return_value=completed_process("radon", stdout=json.dumps({str(file_path): []}))), - ) - - findings = run_radon([file_path]) - - assert "kiss.parameter-count.warning" in {finding.rule for finding in findings} + True, + ), + ( + """ +import typer +app = typer.Typer() -def test_run_radon_exempts_typer_command_context_parameters(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - file_path = tmp_path / "commands.py" - file_path.write_text( - """ @app.command("run") -def callback(ctx: typer.Context, a: str, b: str, c: str, d: str, e: str) -> None: +def callback(a: str, b: str, c: str, d: str, e: str, f: str) -> None: return None """, - encoding="utf-8", - ) - monkeypatch.setattr( - subprocess, - "run", - Mock(return_value=completed_process("radon", stdout=json.dumps({str(file_path): []}))), + False, + ), + ( + """ +@custom.command("run") +def callback(a: str, b: str, c: str, d: str, e: str, f: str) -> None: + return None +""", + True, + ), + ], +) +def test_run_radon_applies_parameter_count_rule_to_cli_decorators( + tmp_path: Path, monkeypatch: MonkeyPatch, source: str, expects_parameter_count_warning: bool +) -> None: + findings = _parameter_count_rules( + tmp_path, + monkeypatch, + source, ) - findings = run_radon([file_path]) - - assert "kiss.parameter-count.warning" not in {finding.rule for finding in findings} + assert ("kiss.parameter-count.warning" in findings) is expects_parameter_count_warning diff --git a/tests/unit/specfact_requirements/test_requirements_runtime.py b/tests/unit/specfact_requirements/test_requirements_runtime.py index 10592288..3f54daea 100644 --- a/tests/unit/specfact_requirements/test_requirements_runtime.py +++ b/tests/unit/specfact_requirements/test_requirements_runtime.py @@ -15,8 +15,11 @@ from specfact_requirements.requirements import runtime as requirements_runtime from specfact_requirements.requirements.runtime import ( RequirementsCoreUnavailableError, + auto_detect_openspec_change, + import_native_requirements_to_bundle, import_requirements_file_to_bundle, list_requirements_with_coverage, + requirements_gate_finding_counts, validate_requirements_bundle, ) @@ -46,6 +49,55 @@ def _bundle_dir(tmp_path: Path) -> Path: return bundle_dir +def _openspec_change(project_root: Path) -> Path: + change_dir = project_root / "openspec" / "changes" / "widget-evidence" + spec_path = change_dir / "specs" / "widgets" / "spec.md" + spec_path.parent.mkdir(parents=True) + (change_dir / "proposal.md").write_text("# Change: Widget evidence\n", encoding="utf-8") + (change_dir / "tasks.md").write_text("# Tasks: Widget evidence\n", encoding="utf-8") + spec_path.write_text( + """## ADDED Requirements + +### Requirement: Widget rendering + +The system SHALL render a widget. + +#### Scenario: Render a valid widget + +- **GIVEN** a valid widget request +- **WHEN** rendering runs +- **THEN** the widget is returned +""", + encoding="utf-8", + ) + return change_dir + + +def _speckit_feature(project_root: Path) -> Path: + feature_dir = project_root / "specs" / "001-widget-rendering" + feature_dir.mkdir(parents=True) + (feature_dir / "spec.md").write_text( + """# Feature Specification: Widget rendering + +## User Scenarios & Testing + +### User Story 1 - Render widgets (Priority: P1) + +As a user, I want widgets rendered so that I can see them. + +**Acceptance Scenarios**: + +1. **Given** a valid widget request, **When** rendering runs, **Then** the widget is returned + +## Requirements + +- **FR-001**: System MUST render a widget +""", + encoding="utf-8", + ) + return feature_dir + + def _block_runtime_import(module_name: str) -> Callable[[str], ModuleType]: def blocked_import(name: str) -> ModuleType: if name == module_name: @@ -156,3 +208,75 @@ def test_list_requirements_with_coverage_is_machine_readable(tmp_path: Path) -> 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 + + +def test_import_openspec_change_persists_core_records_without_mutating_source(tmp_path: Path) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + before = {path.relative_to(change_dir): path.read_bytes() for path in change_dir.rglob("*") if path.is_file()} + bundle_dir = _bundle_dir(project_root) + + result = import_native_requirements_to_bundle("openspec", change_dir, bundle_dir) + + assert [record.requirement_id for record in result.requirements] == [ + "openspec:widget-evidence:widgets:widget-rendering" + ] + assert result.diagnostics == [] + assert list_requirements_with_coverage(bundle_dir)["requirements"][0]["requirement_id"] == ( + "openspec:widget-evidence:widgets:widget-rendering" + ) + after = {path.relative_to(change_dir): path.read_bytes() for path in change_dir.rglob("*") if path.is_file()} + assert after == before + + +def test_import_speckit_feature_persists_core_records(tmp_path: Path) -> None: + project_root = tmp_path / "project" + feature_dir = _speckit_feature(project_root) + bundle_dir = _bundle_dir(project_root) + + result = import_native_requirements_to_bundle("speckit", feature_dir, bundle_dir) + + assert [record.requirement_id for record in result.requirements] == ["speckit:001-widget-rendering:render-a-widget"] + assert result.diagnostics == [] + + +def test_auto_detect_openspec_change_ignores_archive_directory(tmp_path: Path) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + (project_root / "openspec" / "changes" / "archive" / "2026-07-14-widget-evidence").mkdir(parents=True) + + detected = auto_detect_openspec_change(project_root) + + assert detected == change_dir + + +def test_import_rejected_by_core_does_not_persist_partial_sidecar(tmp_path: Path) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + (project_root / "openspec" / "config.yaml").write_text("schema: company-custom\n", encoding="utf-8") + bundle_dir = _bundle_dir(project_root) + + result = import_native_requirements_to_bundle("openspec", change_dir, bundle_dir) + + assert result.requirements == [] + assert [diagnostic.code for diagnostic in result.diagnostics] == ["unsupported-source-schema"] + assert not (bundle_dir / "reports" / "requirements" / "inputs.yaml").exists() + + +def test_profile_resolution_and_gate_counts_delegate_to_core(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + project_root = tmp_path / "project" + change_dir = _openspec_change(project_root) + bundle_dir = _bundle_dir(project_root) + import_native_requirements_to_bundle("openspec", change_dir, bundle_dir) + config_dir = project_root / ".specfact" + config_dir.mkdir() + (config_dir / "config.yaml").write_text("profile: enterprise\n", encoding="utf-8") + monkeypatch.chdir(project_root) + + configured_report = validate_requirements_bundle(bundle_dir) + explicit_report = validate_requirements_bundle(bundle_dir, profile="solo") + gate_counts = requirements_gate_finding_counts(bundle_dir) + + assert configured_report.status == "failed" + assert explicit_report.status == "warnings" + assert gate_counts["scenario-unverified"] == 1