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/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index 74fc3d3f..d179da82 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -66,11 +66,11 @@ AI IDE, rerun, and compare improved evidence. | Order | Change folder | GitHub # | Positioning | Blocked by | |---:|---|---|---|---| -| 1 | `requirements-02-module-commands` | [#165](https://github.com/nold-ai/specfact-cli-modules/issues/165) | Import/normalize requirement context for evidence | core requirements input model | -| 2 | `sync-01-unified-kernel` | [#157](https://github.com/nold-ai/specfact-cli-modules/issues/157) | Preview/apply safety only where validation adapters need it | project/runtime safety specs | -| 3 | `requirements-03-backlog-sync` | [#166](https://github.com/nold-ai/specfact-cli-modules/issues/166) | Read-first backlog drift evidence; no write-back critical path | requirements-02, sync-01 | -| 4 | `architecture-01-solution-layer` | [#164](https://github.com/nold-ai/specfact-cli-modules/issues/164) | Architecture-boundary validation input | core architecture-boundary contracts | -| 5 | `openspec-01-intent-trace` | [#168](https://github.com/nold-ai/specfact-cli-modules/issues/168) | Optional OpenSpec and Spec Kit evidence import adapter | requirements input contracts | +| 1 | `requirements-02-module-commands` | [#165](https://github.com/nold-ai/specfact-cli-modules/issues/165) | Import/normalize requirement context for evidence (shipped via PR #326, archived 2026-07-13) | core requirements input model | +| 2 | `openspec-01-intent-trace` | [#168](https://github.com/nold-ai/specfact-cli-modules/issues/168) | Import-first OpenSpec and Spec Kit requirement evidence runtime with gate surfacing (rescoped 2026-07-13) | requirements-02 runtime; core nold-ai/specfact-cli#350 contracts | +| 3 | `architecture-01-solution-layer` | [#164](https://github.com/nold-ai/specfact-cli-modules/issues/164) | Architecture-boundary validation input | core architecture-boundary contracts | +| 4 | `sync-01-unified-kernel` | [#157](https://github.com/nold-ai/specfact-cli-modules/issues/157) | Preview/apply safety only where validation adapters need it | project/runtime safety specs | +| Parked | `requirements-03-backlog-sync` | [#166](https://github.com/nold-ai/specfact-cli-modules/issues/166) | Read-first backlog drift evidence; no write-back critical path. Deprioritized 2026-07-13 behind openspec-01 | requirements-02, sync-01 | | Gated | `architecture-02-module-well-architected` | [#230](https://github.com/nold-ai/specfact-cli-modules/issues/230) | Architecture-boundary review findings | architecture-01 shipped plus one usage cycle | ### Track C - Supporting Docs @@ -89,7 +89,7 @@ AI IDE, rerun, and compare improved evidence. | `requirements-02-module-commands` | Drop requirement authoring as a flagship workflow. Keep import, normalization, validation, and coverage inspection. | | `requirements-03-backlog-sync` | Keep read-first drift evidence. Write-back remains preview-only and outside the validation critical path. | | `architecture-01-solution-layer` | Keep architecture-boundary input and validation hooks. Drop architecture generation. | -| `openspec-01-intent-trace` | Reframe as optional OpenSpec/Spec Kit adapter consuming upstream artifacts. | +| `openspec-01-intent-trace` | Done 2026-07-13: rescoped to import-first runtime for native OpenSpec/Spec Kit artifacts with deterministic gate surfacing. | | `architecture-02-module-well-architected` | Keep gated until architecture-01 ships and is used for one complete cycle. | | `sync-01-unified-kernel` | Keep only as safety infrastructure for validation/context adapters. | @@ -129,10 +129,10 @@ ceremony rather than validation evidence: ### Wave 3 - Context Adapters - `requirements-02-module-commands` -- `sync-01-unified-kernel` -- `requirements-03-backlog-sync` +- `openspec-01-intent-trace` (pulled forward 2026-07-13, import-first rescope) - `architecture-01-solution-layer` -- `openspec-01-intent-trace` +- `sync-01-unified-kernel` +- `requirements-03-backlog-sync` (parked 2026-07-13) ### Wave 4 - Gated Extensions diff --git a/openspec/changes/requirements-02-module-commands/.openspec.yaml b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/.openspec.yaml similarity index 100% rename from openspec/changes/requirements-02-module-commands/.openspec.yaml rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/.openspec.yaml diff --git a/openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/CHANGE_VALIDATION.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/CHANGE_VALIDATION.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/CHANGE_VALIDATION.md diff --git a/openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/TDD_EVIDENCE.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/TDD_EVIDENCE.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/TDD_EVIDENCE.md diff --git a/openspec/changes/requirements-02-module-commands/design.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/design.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/design.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/design.md diff --git a/openspec/changes/requirements-02-module-commands/proposal.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/proposal.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/proposal.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/proposal.md diff --git a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/backlog-adapter/spec.md similarity index 96% rename from openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/backlog-adapter/spec.md index dae70768..af018730 100644 --- a/openspec/changes/requirements-02-module-commands/specs/backlog-adapter/spec.md +++ b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/backlog-adapter/spec.md @@ -1,4 +1,4 @@ -## MODIFIED Requirements +## ADDED Requirements ### Requirement: Backlog Adapter diff --git a/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/module-io-contract/spec.md similarity index 97% rename from openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/module-io-contract/spec.md index 28f851e8..e35d612f 100644 --- a/openspec/changes/requirements-02-module-commands/specs/module-io-contract/spec.md +++ b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/module-io-contract/spec.md @@ -1,4 +1,4 @@ -## MODIFIED Requirements +## ADDED Requirements ### Requirement: Module Io Contract diff --git a/openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/requirements-module/spec.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/specs/requirements-module/spec.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/specs/requirements-module/spec.md diff --git a/openspec/changes/requirements-02-module-commands/tasks.md b/openspec/changes/archive/2026-07-13-requirements-02-module-commands/tasks.md similarity index 100% rename from openspec/changes/requirements-02-module-commands/tasks.md rename to openspec/changes/archive/2026-07-13-requirements-02-module-commands/tasks.md diff --git a/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md b/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md index 52126942..a290c4c4 100644 --- a/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md +++ b/openspec/changes/openspec-01-intent-trace/CHANGE_VALIDATION.md @@ -1,110 +1,34 @@ # Change Validation Report: openspec-01-intent-trace -**Validation Date**: 2026-03-05 -**Change Proposal**: [proposal.md](./proposal.md) -**Validation Method**: Dry-run simulation — codebase interface analysis + temporary workspace -**Source Plan**: `specfact-cli-internal/docs/internal/implementation/2026-03-05-CLAUDE-RESEARCH-INTENT-DRIVEN-DEVELOPMENT.md` - -## Executive Summary - -- Breaking Changes: 0 detected -- Dependent Files: 3 files affected (additive, non-breaking) -- Impact Level: Low-Medium (extends existing adapter with optional new capability) -- Validation Result: **Pass** -- User Decision: N/A -- Implementation Constraint Identified: 1 (beartype type-annotation — implementation note, not a blocker) - -## Breaking Changes Detected - -None. All interface extensions are additive and optional: -- `parse_change_proposal()` returns `dict[str, Any]` — adding optional `"intent_trace"` key is non-breaking -- `--import-intent` CLI flag has no default effect (opt-in) -- New files (`intent_trace_validator.py`, `intent-trace.schema.json`) have no existing callers - -## Implementation Constraint (Non-Breaking) - -**Constraint**: `_parse_proposal_content()` at `openspec_parser.py:335` has type annotation `dict[str, str]` (return type). If intent trace data (a nested dict) were added here, `@beartype` would raise a type error. - -**Required implementation approach**: Intent trace extraction MUST be done in `parse_change_proposal()` (returns `dict[str, Any]`) by: -1. Calling `_parse_proposal_content(content)` as usual → returns `dict[str, str]` -2. Separately extracting the YAML fenced block under `## Intent Trace` from `content` -3. Parsing with `yaml.safe_load()` and assigning to `result["intent_trace"]` -4. `parse_change_proposal()` return dict is `dict[str, Any]` — no type violation - -The task at step 5.2 ("Add `## Intent Trace` section parser") should be executed in `parse_change_proposal()`, not in `_parse_proposal_content()`. This is an implementation detail — recommend adding a note to `tasks.md`. - -## Dependencies Affected - -### Critical (hard blockers — must land before `--import-intent` write path) - -| Dependency | Issue | Status | -|---|---|---| -| `requirements-01-data-model` | [#238](https://github.com/nold-ai/specfact-cli/issues/238) | PENDING (Wave 5) | -| `requirements-02-module-commands` | [#239](https://github.com/nold-ai/specfact-cli/issues/239) | PENDING (Wave 5/6) | - -Note: The validation/parsing components (JSON Schema, `validate_intent_trace()`, parser extension) do NOT require requirements-01/02. Only the `--import-intent` write path (creating `.req.yaml` files) requires them. This means parsing and validation can be implemented ahead of Wave 5. - -### Recommended Updates (affected, not breaking) - -| File | Reason | Update Type | -|---|---|---| -| `src/specfact_cli/adapters/openspec_parser.py` | Extend `parse_change_proposal()` with intent trace extraction | Additive | -| `src/specfact_cli/adapters/openspec.py` | Extend `_import_change_proposal()` to pass intent trace to bundle; add `--import-intent` path | Additive | -| `src/specfact_cli/validators/change_proposal_integration.py` | May need to call `validate_intent_trace()` when strict mode enabled | Additive | - -## Impact Assessment - -- **Code Impact**: Low — 2 existing files extended (additive only), 2 new files created -- **Test Impact**: Low — new test files only; no existing test modifications required -- **Documentation Impact**: Medium — `docs/adapters/openspec.md` and `docs/guides/openspec-journey.md` need updates -- **Release Impact**: Minor version bump (new features, no breaking changes) - -## Format Validation - -- **proposal.md Format**: Pass - - `# Change:` title ✓ - - `## Why`, `## What Changes`, `## Capabilities`, `## Impact` sections ✓ - - NEW/EXTEND markers ✓ - - Capabilities linked to spec folders ✓ - - Source Tracking section ✓ -- **tasks.md Format**: Pass - - Hierarchical `## 1.`…`## 10.` structure ✓ - - Task 1 = git worktree creation ✓ - - Task 10 = PR creation (last) ✓ - - Post-merge cleanup section ✓ - - TDD / SDD order section at top ✓ - - Tests before implementation (Tasks 2-3 tests before Tasks 4-5 implementation) ✓ - - `TDD_EVIDENCE.md` recording tasks ✓ - - Quality gate tasks ✓ - - Documentation task included ✓ - - Version and changelog task ✓ - - GitHub issue creation task ✓ - - Module signing verification task: not included — this change has no module-package.yaml changes; acceptable -- **specs Format**: Pass - - `####` for scenario headers ✓ - - `## ADDED Requirements` / `## MODIFIED Requirements` delta format ✓ - - G/W/T format with THEN/AND ✓ - - Every requirement has ≥1 scenario ✓ -- **Config.yaml Compliance**: Pass - -## OpenSpec Validation - -- **Status**: Pass -- **Command**: `openspec validate openspec-01-intent-trace --strict` -- **Output**: `Change 'openspec-01-intent-trace' is valid` -- **Issues Found/Fixed**: 0 - -## Dependency Phasing Note - -Unlike ai-integration-04, this change has **two separable implementation phases**: - -1. **Phase A (can land with Wave 5/6)**: JSON Schema definition, `validate_intent_trace()`, parser extension to extract the intent trace block, `openspec validate --strict` hook. No dependency on requirements-01/02. -2. **Phase B (requires Wave 5 complete)**: `--import-intent` flag and `.req.yaml` artifact writer. Requires `BusinessOutcome`/`BusinessRule` Pydantic models. - -Tasks 2-4 can proceed as soon as the worktree is created. Task 5.3 (`--import-intent` flag) should wait for requirements-01 (#238). - -## Ownership Authority - -- **Intent trace schema** (`openspec/schemas/intent-trace.schema.json`): owned by this change; aligns with `requirements-01-data-model` field definitions (no conflict — requirements-01 owns the Pydantic model, this change owns the JSON Schema) -- **`parse_change_proposal()` return dict** (`openspec_parser.py`): existing authority is `openspec_parser.py` itself; this change adds the `"intent_trace"` key — no conflict -- **`--import-intent` write path** in `openspec.py`: this change is authoritative; no other pending change touches this code path +**Status**: VALIDATED — 2026-07-14 (Europe/Berlin) + +## Scope and dependency check + +- 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 22e7db53..9e22bd08 100644 --- a/openspec/changes/openspec-01-intent-trace/design.md +++ b/openspec/changes/openspec-01-intent-trace/design.md @@ -1,82 +1,92 @@ -# Design: OpenSpec Intent Trace — Bridge Adapter Integration +# Design: OpenSpec and Spec Kit Import Runtime for Requirement Evidence ## Context -SpecFact's OpenSpec bridge adapter (`specfact sync bridge --adapter openspec`) reads proposal and task files from an OpenSpec change directory and imports them into SpecFact's project bundle. Currently it reads: title, description, tasks list, and spec references. It does not read any business intent context. This change adds an optional `## Intent Trace` YAML block to the OpenSpec proposal format and teaches the bridge adapter to import it into the `.specfact/requirements/` storage hierarchy used by requirements-01-data-model. - -The principle is: **"OpenSpec owns the intent. SpecFact owns the evidence."** OpenSpec authors write intent in a human-readable YAML block; SpecFact validates conformance and generates evidence. This separation keeps the intent format tool-agnostic. +The `specfact-requirements` module (`packages/specfact-requirements/`) already +wires `requirements import|validate|list|coverage` to core helpers via lazy +imports (`runtime.py` loads `specfact_cli.models.requirements` and +`specfact_cli.requirements.context`). Import currently accepts only +`--from-file`. The core counterpart change (nold-ai/specfact-cli#350) adds +OpenSpec and Spec Kit import normalizers and gate evaluation to core; this +change adds the command-surface runtime. ## Goals / Non-Goals **Goals:** -- Define the `## Intent Trace` section YAML schema and JSON Schema validator -- Extend the OpenSpec bridge adapter to parse and import intent artifacts when the section is present -- Keep the `## Intent Trace` section strictly optional — existing proposals without it are unaffected -- Validate the section against the JSON Schema during `openspec validate --strict` -- Produce `.specfact/requirements/{id}.req.yaml` artifacts from imported intent data + +- Add `--from-openspec [PATH]` and `--from-speckit [PATH]` to + `requirements import`, delegating all parsing/normalization/hashing to core. +- Auto-detect conventional source layouts when the path is omitted. +- Surface core gate findings in `validate` output with non-zero exit when a + profile treats a finding as an error; expose gate-relevant counts in + `list`/`coverage`. **Non-Goals:** -- Forcing all existing OpenSpec proposals to add an `## Intent Trace` section -- Building a new proposal authoring tool — the section is hand-authored YAML in Markdown -- Replacing requirements-01/02 commands — the bridge adapter imports intent; the requirements module validates and traces it + +- Any parsing, hashing, or gate logic in the module (core-owned). +- Writes into upstream OpenSpec or Spec Kit directories. +- Backlog import (deprioritized `requirements-03-backlog-sync` scope). ## Decisions -### D1: YAML fenced block vs structured Markdown headings for Intent Trace +### D1: Flags on `requirements import` vs a new subcommand -**Decision**: YAML fenced block under `## Intent Trace` heading -```yaml -intent_trace: - business_outcomes: - - id: "BO-001" - description: "..." - persona: "..." - business_rules: - - id: "BR-001" - outcome_ref: "BO-001" - given: "..." - when: "..." - then: "..." -``` -**Rationale**: YAML is machine-parseable with a single `yaml.safe_load()` call and maps directly to Pydantic models. Structured Markdown headings require custom parsing logic that is brittle and hard to validate with JSON Schema. YAML fenced blocks are already used in GitHub Actions, Docker Compose, and Kubernetes manifests — authors are familiar with the pattern. -**Alternative rejected**: Structured `### Business Outcomes / ### Business Rules` Markdown sub-sections — readable but not JSON Schema validatable. +**Decision**: Extend the existing `import` command with mutually compatible +source flags (`--from-file`, `--from-openspec`, `--from-speckit`). +**Rationale**: One import surface, one sidecar persistence path, one merge +semantics (`merge_requirement_inputs`). A new subcommand would duplicate the +bundle/sidecar handling for no user benefit. -### D2: JSON Schema stored in SpecFact vs in OpenSpec format repo +### D2: Lazy core-helper loading, same pattern as requirements-02 -**Decision**: JSON Schema at `openspec/schemas/intent-trace.schema.json` within SpecFact's own repo -**Rationale**: SpecFact is the authority for validation. The schema living in SpecFact's repo means the bridge adapter always validates against the version it was built with. When OpenSpec is an external tool (not this repo), the schema reference is still resolvable locally. -**Alternative rejected**: Hosting schema at `openspec.dev/schemas/intent-trace` — external HTTP dependency violates offline-first constraint. +**Decision**: Load the new core import normalizers via the existing +`_load_requirements_module` lazy-import pattern and raise +`RequirementsCoreUnavailableError` with the paired core change named when the +helpers are missing. +**Rationale**: Keeps the module installable against older cores with a clear +error instead of an ImportError traceback. -### D3: `--import-intent` flag vs automatic intent import +### D3: Auto-detection stays conservative -**Decision**: `specfact sync bridge --adapter openspec --import-intent` — opt-in flag -**Rationale**: Not every team using the OpenSpec bridge wants `.specfact/requirements/` files created from every proposal import. The opt-in flag gives teams control. When `## Intent Trace` is present but `--import-intent` is not passed, the bridge still validates the section on `openspec validate --strict` but does not write requirements artifacts. -**Alternative rejected**: Automatic import when section is present — surprising side effect; could overwrite existing requirement files. +**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: `requirement_refs` in tasks.md — free-form vs validated IDs +### D4: Preserve core required-field findings unchanged -**Decision**: Free-form string list (`["BR-001", "AC-002"]`) with advisory validation -**Rationale**: Task-level requirement refs are metadata for traceability, not for enforcement. Advisory-mode validation warns if a ref ID does not match any known `BusinessRule` or `ArchitecturalConstraint` in `.specfact/requirements/` but does not block import. Hard enforcement would break workflows where requirements are not yet captured. +**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: `evidence` field in archived changes +### D5: Preserve core compatibility failures unchanged -**Decision**: Optional string field in change archive metadata: `evidence: ".specfact/evidence/{timestamp}_{run_id}_evidence.json"` -**Rationale**: Minimal — just a file path reference. The evidence file itself is owned by governance-01-evidence-output. The archive metadata is a pointer, not a copy. This keeps the archive lightweight while enabling audit trail navigation. +**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] YAML indentation errors in proposals** — Authors writing `## Intent Trace` blocks manually may introduce YAML syntax errors. Mitigation: `openspec validate --strict` catches YAML parse errors before import; error message shows the line number and suggests a fix. -- **[Risk] ID collision between imported BusinessOutcome IDs and existing requirements** — If a team runs `--import-intent` twice with the same proposal, duplicate `.req.yaml` files may result. Mitigation: bridge adapter checks for existing file with same ID before writing; uses `--overwrite` flag to allow update. -- **[Trade-off] Schema evolution** — As requirements-01-data-model evolves (new fields), the `intent-trace.schema.json` must stay in sync. Mitigation: schema versioning (`schema_version: "1.0"` in the YAML block); bridge adapter rejects unknown schema versions with a clear error. +- **[Risk] Core/module version skew** — flags exist but core helpers are old. + Mitigation: D2 error contract; documented minimum core version in + `module-package.yaml` dependency metadata. +- **[Trade-off] Auto-detection scope** — monorepos with multiple Spec Kit + roots need explicit paths. Accepted; explicit path flag covers it. ## Migration Plan -1. Land requirements-01-data-model (#238) — `BusinessOutcome` and `BusinessRule` Pydantic models must exist. -2. Define `intent-trace.schema.json` using the Pydantic model fields from requirements-01 as source of truth. -3. Extend bridge adapter parser to detect `## Intent Trace` section and extract the YAML block. -4. Implement `--import-intent` flag and requirements artifact writer. -5. Extend `openspec validate --strict` to call JSON Schema validator on intent trace section. -6. Update existing SpecFact dogfood proposals (this repo's `openspec/changes/`) with `## Intent Trace` sections as the team adopts the format. +1. No data migration; sidecar format unchanged. +2. Docs update the requirements module page with import-first examples. ## Open Questions diff --git a/openspec/changes/openspec-01-intent-trace/proposal.md b/openspec/changes/openspec-01-intent-trace/proposal.md index 075a50f4..8e3458f1 100644 --- a/openspec/changes/openspec-01-intent-trace/proposal.md +++ b/openspec/changes/openspec-01-intent-trace/proposal.md @@ -1,39 +1,63 @@ -# Change: OpenSpec and Spec Kit Evidence Import Runtime +# Change: OpenSpec and Spec Kit Import Runtime for Requirement Evidence ## Why -OpenSpec and Spec Kit already own upstream planning artifacts. The modules repo -should provide optional import runtime that maps those artifacts into SpecFact -validation evidence when useful, without requiring upstream tools to adopt a new -intent schema. +OpenSpec and Spec Kit own upstream planning and specification authoring. The +modules repo provides the runtime that imports their native artifacts into +SpecFact requirement evidence, so the `specfact requirements` command group is +useful without hand-authored records files. Core owns the parsing, +normalization, hashing, and gate contracts; this module wires them to commands. -## Ownership Alignment (2026-06-06) +## Rescope (2026-07-13) -- Modules-owned scope retained here: bridge/import runtime, optional metadata - parsing, source attribution, and generated evidence inputs. -- Core-owned scope remains optional adapter contracts and validation behavior. +The previous scope (optional `## Intent Trace` YAML block, schema validation of +hand-authored metadata) is retired: SpecFact must not define an authoring +schema for upstream tools. New scope is **import-first**: parse native OpenSpec +change folders and Spec Kit feature folders as they exist today, read-only, +with deterministic pass/fail validation gates. See the core counterpart +proposal (nold-ai/specfact-cli#350) for contract details. -## What Changes +## Ownership Alignment (2026-07-13) -- **NEW**: Optional import support for OpenSpec proposals and Spec Kit feature - folders. -- **NEW**: Source-attributed mapping of tasks, spec deltas, acceptance checks, - requirement references, and evidence links when present. -- **NEW**: Strict metadata validation only when optional metadata exists. -- **EXTEND**: Project sync/import commands avoid duplicate planning artifacts and - instead feed validation evidence. +- Modules-owned scope retained here: `requirements import` runtime flags + (`--from-openspec`, `--from-speckit`), source auto-detection, command wiring, + and surfacing of gate findings in validate/list/coverage output. +- Core-owned scope remains: artifact parsers, normalization into + `RequirementInput`, content-hash staleness contract, and gate evaluation. +- Runtime MUST stay thin: no parsing, hashing, or gate logic in this module. +- Runtime MUST NOT create or mutate upstream OpenSpec or Spec Kit artifacts. -## Capabilities +## What Changes -### New Capabilities +- **NEW**: `specfact requirements import --from-openspec [PATH]` imports native + OpenSpec change folders through the core evidence adapter. +- **NEW**: `specfact requirements import --from-speckit [PATH]` imports native + Spec Kit feature folders through the core evidence adapter. +- **NEW**: Omitted paths auto-detect conventional layouts (`openspec/changes/` + 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. +- **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. -- `openspec-speckit-evidence-import-runtime`: Optional runtime adapter for - OpenSpec and Spec Kit artifacts consumed by validation. +## Capabilities ### Modified Capabilities -- `openspec-bridge-adapter`: Extended to parse optional metadata and evidence - links without requiring them. +- `requirements-module`: Runtime commands gain OpenSpec and Spec Kit import + sources and gate-finding surfacing. --- @@ -43,5 +67,5 @@ intent schema. - **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/openspec-bridge-adapter/spec.md b/openspec/changes/openspec-01-intent-trace/specs/openspec-bridge-adapter/spec.md deleted file mode 100644 index 2012e709..00000000 --- a/openspec/changes/openspec-01-intent-trace/specs/openspec-bridge-adapter/spec.md +++ /dev/null @@ -1,22 +0,0 @@ -## MODIFIED Requirements - -### Requirement: OpenSpec Bridge Adapter Import -The system SHALL import OpenSpec change proposals into SpecFact's project bundle with full backwards compatibility when the `## Intent Trace` section is absent, and include intent context when the section is present. - -#### Scenario: Proposal import without Intent Trace section is unchanged -- **GIVEN** an OpenSpec proposal that has no `## Intent Trace` section -- **WHEN** `specfact sync bridge --adapter openspec` is run -- **THEN** the import behaviour is identical to the pre-change behaviour -- **AND** no error, warning, or advisory is emitted related to missing intent trace - -#### Scenario: Proposal import includes intent context when section is present -- **GIVEN** an OpenSpec proposal with a valid `## Intent Trace` section -- **WHEN** `specfact sync bridge --adapter openspec` is run (without `--import-intent`) -- **THEN** the proposal's intent trace metadata is attached to the project bundle as read-only context -- **AND** `specfact project health-check` can report that intent context is available for the change - -#### Scenario: `openspec validate --strict` validates intent trace when present -- **GIVEN** an OpenSpec change with a proposal containing a `## Intent Trace` section -- **WHEN** `openspec validate --strict` is run -- **THEN** the validator checks the YAML block against `intent-trace.schema.json` -- **AND** any schema violations cause a non-zero exit code with descriptive error messages diff --git a/openspec/changes/openspec-01-intent-trace/specs/openspec-bridge-intent-import/spec.md b/openspec/changes/openspec-01-intent-trace/specs/openspec-bridge-intent-import/spec.md deleted file mode 100644 index 6d04cad0..00000000 --- a/openspec/changes/openspec-01-intent-trace/specs/openspec-bridge-intent-import/spec.md +++ /dev/null @@ -1,49 +0,0 @@ -## ADDED Requirements - -### Requirement: Bridge Adapter Intent Import -The system SHALL extend `specfact sync bridge --adapter openspec` with an `--import-intent` flag that reads the `## Intent Trace` YAML block from imported proposals and creates corresponding `.specfact/requirements/{id}.req.yaml` artifacts. - -#### Scenario: Intent import creates BusinessOutcome artifacts -- **GIVEN** an OpenSpec proposal with a `## Intent Trace` section containing at least one `business_outcomes` entry -- **WHEN** `specfact sync bridge --adapter openspec --import-intent` is run -- **THEN** a `.specfact/requirements/{id}.req.yaml` file is created for each `BusinessOutcome` in the intent trace -- **AND** each artifact validates against the `BusinessOutcome` Pydantic schema without errors - -#### Scenario: Intent import creates BusinessRule artifacts -- **GIVEN** an OpenSpec proposal with `business_rules` entries in the `## Intent Trace` section -- **WHEN** `specfact sync bridge --adapter openspec --import-intent` is run -- **THEN** each `BusinessRule` (id, outcome_ref, given, when, then) is stored in the corresponding `.req.yaml` artifact under its parent `BusinessOutcome` -- **AND** the `outcome_ref` is resolved to a valid `BusinessOutcome` ID in the imported requirements - -#### Scenario: Intent import skips existing artifacts without --overwrite -- **GIVEN** a `.specfact/requirements/BO-001.req.yaml` file already exists -- **WHEN** `specfact sync bridge --adapter openspec --import-intent` is run without `--overwrite` -- **THEN** the existing file is not modified -- **AND** the CLI output notes the skipped artifact with its ID - -#### Scenario: Intent import overwrites with --overwrite flag -- **GIVEN** a `.specfact/requirements/BO-001.req.yaml` file already exists -- **WHEN** `specfact sync bridge --adapter openspec --import-intent --overwrite` is run -- **THEN** the existing file is updated with the content from the proposal's intent trace section -- **AND** the CLI output confirms the overwritten artifact ID - -#### Scenario: Import without --import-intent ignores intent trace section -- **GIVEN** an OpenSpec proposal with a `## Intent Trace` section -- **WHEN** `specfact sync bridge --adapter openspec` is run without `--import-intent` -- **THEN** no `.specfact/requirements/` artifacts are created -- **AND** the section is validated but not imported - -### Requirement: Task-Level Requirement References -The system SHALL support an optional `requirement_refs` list field on individual tasks in OpenSpec `tasks.md` files, linking tasks to specific `BusinessRule` or `ArchitecturalConstraint` IDs. - -#### Scenario: Bridge adapter parses requirement_refs in tasks -- **GIVEN** a `tasks.md` file with a task containing `requirement_refs: ["BR-001", "AC-002"]` -- **WHEN** the bridge adapter imports the proposal -- **THEN** the imported task record includes the requirement ref IDs -- **AND** they are included in the project bundle's task metadata - -#### Scenario: Advisory validation warns on unresolved requirement refs -- **GIVEN** a task with `requirement_refs: ["BR-999"]` where BR-999 does not exist in `.specfact/requirements/` -- **WHEN** `specfact sync bridge --adapter openspec` is run -- **THEN** the CLI emits an advisory warning: `[ADVISORY] Task X: requirement_refs contains unknown ID BR-999` -- **AND** the import proceeds without failing diff --git a/openspec/changes/openspec-01-intent-trace/specs/openspec-intent-trace-schema/spec.md b/openspec/changes/openspec-01-intent-trace/specs/openspec-intent-trace-schema/spec.md deleted file mode 100644 index b9fa93ae..00000000 --- a/openspec/changes/openspec-01-intent-trace/specs/openspec-intent-trace-schema/spec.md +++ /dev/null @@ -1,42 +0,0 @@ -## ADDED Requirements - -### Requirement: Intent Trace Section Schema -The system SHALL define a JSON Schema at `openspec/schemas/intent-trace.schema.json` that validates the `## Intent Trace` YAML block in OpenSpec proposal files. - -#### Scenario: Valid intent trace section passes schema validation -- **GIVEN** an OpenSpec proposal with a correctly structured `## Intent Trace` YAML block -- **WHEN** `openspec validate --strict` is run -- **THEN** the intent trace section validates without errors -- **AND** the validation output confirms intent trace section is present and valid - -#### Scenario: Invalid intent trace section fails schema validation -- **GIVEN** an OpenSpec proposal with a `## Intent Trace` YAML block missing a required field (e.g., `id` on a `BusinessOutcome`) -- **WHEN** `openspec validate --strict` is run -- **THEN** the validation exits with a non-zero code -- **AND** the error message identifies the specific field violation and the line context - -#### Scenario: Missing intent trace section is valid (section is optional) -- **GIVEN** an OpenSpec proposal without any `## Intent Trace` section -- **WHEN** `openspec validate --strict` is run -- **THEN** the validation passes without intent-trace errors -- **AND** no warning about missing intent trace is emitted in normal mode - -#### Scenario: Intent trace schema includes schema version field -- **GIVEN** an intent trace YAML block with `schema_version: "1.0"` -- **WHEN** the bridge adapter reads the block -- **THEN** it accepts the artifact and records the schema version -- **AND** if the schema version is unknown the adapter emits a clear error with supported versions - -### Requirement: Intent Trace Evidence Field in Archive -The system SHALL support an optional `evidence` field in change archive metadata pointing to the evidence JSON envelope file produced during implementation. - -#### Scenario: Archive metadata includes evidence reference -- **GIVEN** an archived change that generated a governance evidence artifact -- **WHEN** the archive metadata is read -- **THEN** the `evidence` field contains a relative path to the `.specfact/evidence/` JSON file -- **AND** the path resolves to a readable file on disk - -#### Scenario: Archive without evidence field is valid -- **GIVEN** an archived change that did not produce governance evidence -- **WHEN** the archive metadata is validated -- **THEN** validation passes without errors related to the missing evidence field 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 new file mode 100644 index 00000000..81bb814f --- /dev/null +++ b/openspec/changes/openspec-01-intent-trace/specs/requirements-module/spec.md @@ -0,0 +1,66 @@ +## ADDED Requirements + +### Requirement: OpenSpec and Spec Kit Import Runtime + +The requirements module SHALL provide runtime command flags that import native +OpenSpec change folders and Spec Kit feature folders into requirement evidence +through the core evidence adapter, and SHALL surface core gate findings in +validation output. The runtime SHALL contain no parsing, hashing, or gate +logic of its own and SHALL never write into upstream artifact directories. + +#### Scenario: Import from an OpenSpec change folder + +- **GIVEN** a project bundle and an OpenSpec change folder +- **WHEN** `specfact requirements import --from-openspec --bundle ` runs +- **THEN** the core adapter normalizes the artifacts into requirement records +- **AND** merged records persist to the bundle requirements sidecar exactly like `--from-file` imports +- **AND** the command reports imported counts and diagnostics. + +#### Scenario: Import from a Spec Kit feature folder + +- **GIVEN** a project bundle and a Spec Kit feature folder +- **WHEN** `specfact requirements import --from-speckit --bundle ` runs +- **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 active conventional layouts + +- **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 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 + +- **GIVEN** a bundle with imported requirements that trigger core gate findings + (`scenario-unverified`, `stale-import`, `source-missing`, `ambiguous-mapping`) +- **WHEN** `specfact requirements validate --bundle --profile ` runs +- **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 +- **WHEN** the commands complete +- **THEN** the upstream source directories are byte-identical to before the run. diff --git a/openspec/changes/openspec-01-intent-trace/tasks.md b/openspec/changes/openspec-01-intent-trace/tasks.md index fc8d320c..f46ff159 100644 --- a/openspec/changes/openspec-01-intent-trace/tasks.md +++ b/openspec/changes/openspec-01-intent-trace/tasks.md @@ -1,151 +1,42 @@ -# Tasks: OpenSpec Intent Trace — Bridge Adapter Integration +# Tasks: OpenSpec and Spec Kit Import Runtime for Requirement Evidence ## TDD / SDD order (enforced) -Per `openspec/config.yaml`, tests MUST precede production code for any behavior-changing task. - -Order: -1. Spec deltas (already in `specs/`) -2. Tests derived from spec scenarios — run and expect failure -3. Production code — implement until tests pass - -Do not implement production code until tests exist and have been run (expecting failure). +Spec deltas first, failing tests second, production code third. Record +evidence in `TDD_EVIDENCE.md`. --- -## 1. Create git worktree for this change - -- [ ] 1.1 Fetch latest and create a worktree with a new branch from `origin/dev`. - - [ ] 1.1.1 `git fetch origin` - - [ ] 1.1.2 `git worktree add ../specfact-cli-worktrees/feature/openspec-01-intent-trace -b feature/openspec-01-intent-trace origin/dev` - - [ ] 1.1.3 `cd ../specfact-cli-worktrees/feature/openspec-01-intent-trace` - - [ ] 1.1.4 `python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"` - - [ ] 1.1.5 `git branch --show-current` (verify `feature/openspec-01-intent-trace`) - -## 2. Define Intent Trace JSON Schema - -- [ ] 2.1 Create `openspec/schemas/intent-trace.schema.json`: - - [ ] 2.1.1 Root object with `schema_version` (required string), `intent_trace` object - - [ ] 2.1.2 `business_outcomes` array: each item requires `id` (string), `description` (string), `persona` (string) - - [ ] 2.1.3 `business_rules` array: each item requires `id`, `outcome_ref`, `given`, `when`, `then` - - [ ] 2.1.4 `architectural_constraints` array: each item requires `id`, `outcome_ref`, `constraint` - - [ ] 2.1.5 `requirement_refs` optional array of strings - - [ ] 2.1.6 `additionalProperties: false` on all objects for strict validation -- [ ] 2.2 Write schema unit tests in `tests/unit/specfact_cli/test_intent_trace_schema.py`: - - [ ] 2.2.1 Test valid complete intent trace block passes validation - - [ ] 2.2.2 Test missing required `id` field on BusinessOutcome fails - - [ ] 2.2.3 Test missing intent trace section (None) passes (optional) - - [ ] 2.2.4 Test unknown `schema_version` raises descriptive error -- [ ] 2.3 Run schema tests — expect failure: `hatch test -- tests/unit/specfact_cli/test_intent_trace_schema.py -v` -- [ ] 2.4 Record failing test evidence in `TDD_EVIDENCE.md` - -## 3. Write bridge adapter tests (TDD — expect failure) - -- [ ] 3.1 Review existing OpenSpec bridge adapter tests in `tests/` -- [ ] 3.2 Add `tests/unit/specfact_cli/test_openspec_bridge_intent.py`: - - [ ] 3.2.1 Test bridge import of proposal with `## Intent Trace` creates `.req.yaml` files (with `--import-intent`) - - [ ] 3.2.2 Test bridge import without `## Intent Trace` section is unchanged (backwards compatible) - - [ ] 3.2.3 Test `--import-intent` without `--overwrite` skips existing artifacts - - [ ] 3.2.4 Test `--import-intent --overwrite` updates existing artifacts - - [ ] 3.2.5 Test advisory warning on unresolved `requirement_refs` - - [ ] 3.2.6 Test `requirement_refs` parsed into imported task metadata -- [ ] 3.3 Add `tests/integration/test_openspec_intent_trace_e2e.py`: - - [ ] 3.3.1 End-to-end test: proposal with intent trace → bridge import → `.req.yaml` exists and validates -- [ ] 3.4 Run bridge tests — expect failure: `hatch test -- tests/unit/specfact_cli/test_openspec_bridge_intent.py -v` -- [ ] 3.5 Record failing test evidence in `TDD_EVIDENCE.md` - -## 4. Implement Intent Trace schema validator - -- [ ] 4.1 Add `src/specfact_cli/validators/intent_trace_validator.py`: - - [ ] 4.1.1 `validate_intent_trace(yaml_block: dict | None) -> ValidationResult` with `@require` and `@beartype` - - [ ] 4.1.2 Load `openspec/schemas/intent-trace.schema.json` (bundled resource) - - [ ] 4.1.3 Use `jsonschema.validate()` for schema check - - [ ] 4.1.4 Return structured errors with field path, message, and suggestion -- [ ] 4.2 Register schema file in `pyproject.toml` `[tool.hatch.build.targets.wheel]` force-include - -## 5. Extend OpenSpec bridge adapter with intent import - -> **Implementation constraint (from CHANGE_VALIDATION.md)**: `_parse_proposal_content()` in `openspec_parser.py` has return type `dict[str, str]`. Intent trace extraction MUST be done in `parse_change_proposal()` (returns `dict[str, Any]`) — NOT in `_parse_proposal_content()` — to avoid a `@beartype` type violation. - -- [ ] 5.1 Locate OpenSpec bridge adapter in `src/specfact_cli/adapters/` -- [ ] 5.2 Add `## Intent Trace` section parser: - - [ ] 5.2.1 Extract YAML fenced block under `## Intent Trace` heading from proposal Markdown - - [ ] 5.2.2 Parse YAML with `yaml.safe_load()` - - [ ] 5.2.3 Run `validate_intent_trace()` on the parsed block -- [ ] 5.3 Add `--import-intent` flag to `specfact sync bridge --adapter openspec` command: - - [ ] 5.3.1 `@require`: intent import requires requirements-01 module is installed (advisory check) - - [ ] 5.3.2 Write `.specfact/requirements/{id}.req.yaml` for each `BusinessOutcome` - - [ ] 5.3.3 Embed `BusinessRule` entries in parent `.req.yaml` files - - [ ] 5.3.4 Respect `--overwrite` flag; skip existing files otherwise -- [ ] 5.4 Add `requirement_refs` parsing from `tasks.md` task entries - - [ ] 5.4.1 Parse optional `requirement_refs:` YAML field on task lines - - [ ] 5.4.2 Include in imported task metadata in project bundle - - [ ] 5.4.3 Advisory warning for unresolved IDs -- [ ] 5.5 Extend `openspec validate --strict` hook to call `validate_intent_trace()` when section present -- [ ] 5.6 Add `@require`, `@ensure`, `@beartype` decorators to all new public API functions - -## 6. Passing tests and quality gates - -- [ ] 6.1 Run all new tests — expect passing: `hatch test -- tests/unit/specfact_cli/test_intent_trace*.py tests/unit/specfact_cli/test_openspec_bridge*.py tests/integration/test_openspec_intent_trace*.py -v` -- [ ] 6.2 Record passing test evidence in `TDD_EVIDENCE.md` -- [ ] 6.3 `hatch run format` -- [ ] 6.4 `hatch run type-check` -- [ ] 6.5 `hatch run lint` -- [ ] 6.6 `hatch run yaml-lint` -- [ ] 6.7 `hatch run contract-test` -- [ ] 6.8 `hatch run smart-test` - -## 7. Documentation +## 1. Branch and dependency guardrails -- [ ] 7.1 Update `docs/adapters/openspec.md` (or equivalent): - - [ ] 7.1.1 Document `## Intent Trace` section format with full YAML example - - [ ] 7.1.2 Document `--import-intent` and `--overwrite` flags - - [ ] 7.1.3 Document `requirement_refs` field on tasks -- [ ] 7.2 Update `docs/guides/openspec-journey.md` — add Intent Trace section -- [ ] 7.3 Ensure front-matter on all updated/new doc pages is valid (layout, title, permalink, description) -- [ ] 7.4 Update `docs/_layouts/default.html` sidebar navigation if new pages are added +- [ ] 1.1 Create worktree branch `feature/openspec-01-intent-trace` from `dev`. +- [ ] 1.2 Verify the paired core change (nold-ai/specfact-cli#350) import normalizers and gate helpers are merged, or coordinate parallel implementation with explicit core version pinning. +- [ ] 1.3 Re-run change validation for the rescoped proposal and refresh `CHANGE_VALIDATION.md`. -## 8. Version and changelog +## 2. Spec-first and test-first preparation -- [ ] 8.1 Bump minor version in `pyproject.toml`, `setup.py`, `src/__init__.py`, `src/specfact_cli/__init__.py` -- [ ] 8.2 Add CHANGELOG.md entry under new `[X.Y.Z] - 2026-XX-XX` with Added/Changed sections +- [ ] 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; 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`. -## 9. GitHub issue creation +## 3. Implementation -- [ ] 9.1 Create GitHub issue: - ```bash - gh issue create \ - --repo nold-ai/specfact-cli \ - --title "[Change] OpenSpec Intent Trace — Bridge Adapter Integration" \ - --body-file /tmp/github-issue-openspec-01.md \ - --label "enhancement" \ - --label "change-proposal" - ``` -- [ ] 9.2 Link issue to project: `gh project item-add 1 --owner nold-ai --url ` -- [ ] 9.3 Update `proposal.md` Source Tracking section with issue number and URL -- [ ] 9.4 Link branch to issue: `gh issue develop --repo nold-ai/specfact-cli --name feature/openspec-01-intent-trace` +- [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. -## 10. Pull request +## 4. Validation and documentation -- [ ] 10.1 `git add` all changed files; commit with `feat: add OpenSpec Intent Trace section and bridge adapter import` -- [ ] 10.2 `git push -u origin feature/openspec-01-intent-trace` -- [ ] 10.3 Create PR: - ```bash - gh pr create \ - --repo nold-ai/specfact-cli \ - --base dev \ - --head feature/openspec-01-intent-trace \ - --title "feat: OpenSpec Intent Trace bridge adapter integration" \ - --body-file /tmp/pr-body-openspec-01.md - ``` -- [ ] 10.4 Link PR to project: `gh project item-add 1 --owner nold-ai --url ` -- [ ] 10.5 Set project status to "In Progress" +- [ ] 4.1 Re-run tests and quality gates until green; record passing evidence in `TDD_EVIDENCE.md`. +- [ ] 4.2 Update module docs and command overview with import-first examples and gate categories. +- [ ] 4.3 Run `openspec validate openspec-01-intent-trace --strict` and resolve all issues. -## Post-merge cleanup (after PR is merged) +## 5. Delivery -- [ ] Return to primary checkout: `cd .../specfact-cli` -- [ ] `git fetch origin` -- [ ] `git worktree remove ../specfact-cli-worktrees/feature/openspec-01-intent-trace` -- [ ] `git branch -d feature/openspec-01-intent-trace` -- [ ] `git worktree prune` -- [ ] (Optional) `git push origin --delete feature/openspec-01-intent-trace` +- [ ] 5.1 Open PR to `dev` with spec/test/code/docs evidence. +- [ ] 5.2 Sync GitHub issue #168 title/body with the rescoped proposal. diff --git a/openspec/specs/backlog-adapter/spec.md b/openspec/specs/backlog-adapter/spec.md new file mode 100644 index 00000000..853d0970 --- /dev/null +++ b/openspec/specs/backlog-adapter/spec.md @@ -0,0 +1,24 @@ +# backlog-adapter Specification + +## Purpose +TBD - created by archiving change requirements-02-module-commands. Update Purpose after archive. +## Requirements +### Requirement: Backlog Adapter + +The system SHALL expose source-attributed backlog requirement snippets to +requirements import workflows. + +#### Scenario: Adapter returns acceptance criteria payload for import + +- **GIVEN** a backlog item selected for requirement context import +- **WHEN** requirements import receives adapter source fields +- **THEN** title, description, acceptance-criteria text, and item identity are available +- **AND** normalization proceeds without provider-specific parsing in command handlers. + +#### Scenario: Missing acceptance criteria is surfaced explicitly + +- **GIVEN** a backlog item with no acceptance criteria +- **WHEN** normalization runs +- **THEN** the item is reported as incomplete input +- **AND** command output includes the backlog item identifier. + diff --git a/openspec/specs/module-io-contract/spec.md b/openspec/specs/module-io-contract/spec.md new file mode 100644 index 00000000..6dfcf33e --- /dev/null +++ b/openspec/specs/module-io-contract/spec.md @@ -0,0 +1,24 @@ +# module-io-contract Specification + +## Purpose +TBD - created by archiving change requirements-02-module-commands. Update Purpose after archive. +## Requirements +### Requirement: Module Io Contract + +The requirements module SHALL consume core requirements context adapter helpers +through the existing `ModuleIOContract` boundary. + +#### Scenario: Import operation maps backlog items to requirements + +- **GIVEN** source-attributed requirement records imported by a module command +- **WHEN** the runtime stores normalized requirements on a `ProjectBundle` +- **THEN** requirements are added under the `requirements.inputs` extension with stable IDs +- **AND** parse diagnostics remain available to the module runtime for partial failures. + +#### Scenario: Validate operation enforces profile schema + +- **GIVEN** a requirements bundle and active validation profile +- **WHEN** the runtime delegates to core requirements context validation +- **THEN** missing evidence links and weak context are reported +- **AND** validation severity respects the selected evidence strictness profile. + diff --git a/openspec/specs/requirements-module/spec.md b/openspec/specs/requirements-module/spec.md new file mode 100644 index 00000000..3edb4b99 --- /dev/null +++ b/openspec/specs/requirements-module/spec.md @@ -0,0 +1,39 @@ +# requirements-module Specification + +## Purpose +TBD - created by archiving change requirements-02-module-commands. Update Purpose after archive. +## Requirements +### Requirement: Requirements Runtime Commands + +The system SHALL provide module-owned `specfact requirements ...` commands for +importing, validating, listing, and inspecting upstream requirement context as +validation evidence. + +#### Scenario: Import local requirement records into bundle extensions + +- **GIVEN** a local JSON or YAML file containing source-attributed requirement records +- **WHEN** `specfact requirements import --from-file --bundle ` runs +- **THEN** valid records are stored under the bundle's `requirements.inputs` extension +- **AND** invalid records are returned as bounded diagnostics. + +#### Scenario: Validate requirement context by profile + +- **GIVEN** a project bundle with normalized requirement inputs +- **WHEN** `specfact requirements validate --bundle --profile enterprise` runs +- **THEN** validation delegates to the core profile-aware requirements context helper +- **AND** missing downstream evidence links are reported as failed validation. + +#### Scenario: List requirements with coverage summary + +- **GIVEN** requirement inputs are present on a project bundle +- **WHEN** `specfact requirements list --bundle --show-coverage --format json` runs +- **THEN** each requirement ID and title is returned +- **AND** the output includes a machine-readable coverage summary. + +#### Scenario: No authoring command is exposed + +- **GIVEN** the requirements module is installed +- **WHEN** the user inspects `specfact requirements --help` +- **THEN** import, validate, list, and coverage commands are visible +- **AND** requirement authoring commands are not exposed. + diff --git a/packages/specfact-code-review/module-package.yaml b/packages/specfact-code-review/module-package.yaml index 0e62afec..f625bf58 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.52 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:2fc7b2456493245328c294008e17a72a224edfeb7c3ca2fc581b5de94fe941ca + signature: QJB9dXYvdTTzboXBRGGqMXoEKvoVdVwK/B9NVZ/xk2F8QwusMkXMGVqmQzF+biH4xIaujqK94wqw30s2yIUMDQ== 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..93cf2735 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.4 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:ed53370ebcfb4a0667e5fc7e0af097d0dd4896fc70b583965664ccc833d1e8b3 + signature: piqKk1AXxSRms2k+hGRn8IHFQE4g60M14qU/rDdChn++sq/HBCj7S9fNq1LfhyKD0TfendIbz01Is+dIoB1cDw== 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..c350f2f4 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.52", + "download_url": "modules/specfact-code-review-0.47.52.tar.gz", + "checksum_sha256": "b946e680f38672f443312821a511d4ce05f3b4385c1f0432604c4cef652f8547", "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.4", + "download_url": "modules/specfact-requirements-0.2.4.tar.gz", + "checksum_sha256": "3d8751f3b15624262c24079900056d681b0668f24d5f69f9041c5b8096d370b3", "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-code-review-0.47.52.tar.gz b/registry/modules/specfact-code-review-0.47.52.tar.gz new file mode 100644 index 00000000..bda26391 Binary files /dev/null and b/registry/modules/specfact-code-review-0.47.52.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.47.52.tar.gz.sha256 b/registry/modules/specfact-code-review-0.47.52.tar.gz.sha256 new file mode 100644 index 00000000..983fdf92 --- /dev/null +++ b/registry/modules/specfact-code-review-0.47.52.tar.gz.sha256 @@ -0,0 +1 @@ +b946e680f38672f443312821a511d4ce05f3b4385c1f0432604c4cef652f8547 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/registry/modules/specfact-requirements-0.2.4.tar.gz b/registry/modules/specfact-requirements-0.2.4.tar.gz new file mode 100644 index 00000000..da413ab8 Binary files /dev/null and b/registry/modules/specfact-requirements-0.2.4.tar.gz differ diff --git a/registry/modules/specfact-requirements-0.2.4.tar.gz.sha256 b/registry/modules/specfact-requirements-0.2.4.tar.gz.sha256 new file mode 100644 index 00000000..5069ba93 --- /dev/null +++ b/registry/modules/specfact-requirements-0.2.4.tar.gz.sha256 @@ -0,0 +1 @@ +3d8751f3b15624262c24079900056d681b0668f24d5f69f9041c5b8096d370b3 diff --git a/registry/signatures/specfact-code-review-0.47.52.tar.sig b/registry/signatures/specfact-code-review-0.47.52.tar.sig new file mode 100644 index 00000000..61144493 --- /dev/null +++ b/registry/signatures/specfact-code-review-0.47.52.tar.sig @@ -0,0 +1 @@ +QJB9dXYvdTTzboXBRGGqMXoEKvoVdVwK/B9NVZ/xk2F8QwusMkXMGVqmQzF+biH4xIaujqK94wqw30s2yIUMDQ== diff --git a/registry/signatures/specfact-requirements-0.2.4.tar.sig b/registry/signatures/specfact-requirements-0.2.4.tar.sig new file mode 100644 index 00000000..c6c23f98 --- /dev/null +++ b/registry/signatures/specfact-requirements-0.2.4.tar.sig @@ -0,0 +1 @@ +piqKk1AXxSRms2k+hGRn8IHFQE4g60M14qU/rDdChn++sq/HBCj7S9fNq1LfhyKD0TfendIbz01Is+dIoB1cDw== 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