diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d3e7030f..e78b550d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -35,8 +35,9 @@ Paste command output snippets or link workflow runs. - [ ] `hatch run lint` - [ ] `hatch run yaml-lint` - [ ] `hatch run check-bundle-imports` -- [ ] `hatch run contract-test` -- [ ] `hatch run smart-test` (or `hatch run test`) +- [ ] `hatch run contract-test-contracts` +- [ ] `hatch run smart-test-check` +- [ ] `hatch run test` ### Signature + version integrity (required) diff --git a/.github/workflows/docs-review.yml b/.github/workflows/docs-review.yml index 7c6f9cbc..42b8df62 100644 --- a/.github/workflows/docs-review.yml +++ b/.github/workflows/docs-review.yml @@ -9,10 +9,14 @@ on: - "**/*.md" - "**/*.mdc" - "docs/**" - - "packages/*/resources/prompts/**" + - "packages/*/resources/**" - "requirements-docs-ci.txt" - "scripts/check-docs-commands.py" - "scripts/check-prompt-commands.py" + - "scripts/check-command-contract.py" + - "scripts/generate-command-overview.py" + - "docs/reference/commands.generated.*" + - "llms.txt" - "scripts/docs_site_validation.py" - "tests/unit/test_check_docs_commands_script.py" - "tests/unit/test_check_prompt_commands_script.py" @@ -25,10 +29,14 @@ on: - "**/*.md" - "**/*.mdc" - "docs/**" - - "packages/*/resources/prompts/**" + - "packages/*/resources/**" - "requirements-docs-ci.txt" - "scripts/check-docs-commands.py" - "scripts/check-prompt-commands.py" + - "scripts/check-command-contract.py" + - "scripts/generate-command-overview.py" + - "docs/reference/commands.generated.*" + - "llms.txt" - "scripts/docs_site_validation.py" - "tests/unit/test_check_docs_commands_script.py" - "tests/unit/test_check_prompt_commands_script.py" @@ -50,6 +58,37 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Resolve paired core command sources ref + id: core-ref + shell: bash + env: + CANDIDATE_REF: ${{ github.head_ref || github.ref_name }} + FALLBACK_REF: ${{ github.base_ref || github.ref_name }} + run: | + candidate="${CANDIDATE_REF}" + if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/nold-ai/specfact-cli.git "$candidate" >/dev/null 2>&1; then + echo "ref=$candidate" >> "$GITHUB_OUTPUT" + else + fallback="${FALLBACK_REF:-dev}" + case "$fallback" in + main|dev) ;; + *) fallback="dev" ;; + esac + echo "ref=$fallback" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout paired core command sources + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: nold-ai/specfact-cli + path: specfact-cli + ref: ${{ steps.core-ref.outputs.ref }} + persist-credentials: false + + - name: Export paired core source path + run: echo "SPECFACT_CLI_REPO=${GITHUB_WORKSPACE}/specfact-cli" >> "$GITHUB_ENV" - name: Set up Python 3.12 uses: actions/setup-python@v5 @@ -60,6 +99,7 @@ jobs: - name: Install docs review dependencies run: | python -m pip install --upgrade pip + python -m pip install hatch python -m pip install -r requirements-docs-ci.txt - name: Run docs review suite @@ -81,6 +121,11 @@ jobs: mkdir -p logs/docs-review PROMPT_COMMAND_LOG="logs/docs-review/prompt-command-validation_$(date -u +%Y%m%d_%H%M%S).log" python scripts/check-prompt-commands.py 2>&1 | tee "$PROMPT_COMMAND_LOG" + + - name: Validate generated command overview + run: | + hatch run check-command-overview + hatch run check-command-contract exit "${PIPESTATUS[0]:-$?}" - name: Upload docs review logs diff --git a/.github/workflows/pr-orchestrator.yml b/.github/workflows/pr-orchestrator.yml index 18037a52..e920dc52 100644 --- a/.github/workflows/pr-orchestrator.yml +++ b/.github/workflows/pr-orchestrator.yml @@ -125,6 +125,39 @@ jobs: - name: Checkout if: needs.changes.outputs.skip_tests_dev_to_main != 'true' uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Resolve paired core CLI ref + id: core-ref + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' + shell: bash + env: + CANDIDATE_REF: ${{ github.head_ref || github.ref_name }} + FALLBACK_REF: ${{ github.base_ref || github.ref_name }} + run: | + candidate="${CANDIDATE_REF}" + if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/nold-ai/specfact-cli.git "$candidate" >/dev/null 2>&1; then + echo "ref=$candidate" >> "$GITHUB_OUTPUT" + else + fallback="${FALLBACK_REF:-dev}" + case "$fallback" in + main|dev) ;; + *) fallback="dev" ;; + esac + echo "ref=$fallback" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout paired core CLI + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: nold-ai/specfact-cli + path: specfact-cli + ref: ${{ steps.core-ref.outputs.ref }} + persist-credentials: false + - name: Export paired core CLI path + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' + run: echo "SPECFACT_CLI_REPO=${GITHUB_WORKSPACE}/specfact-cli" >> "$GITHUB_ENV" - name: Setup Python if: needs.changes.outputs.skip_tests_dev_to_main != 'true' uses: actions/setup-python@v5 @@ -140,7 +173,17 @@ jobs: run: hatch env create - name: Install specfact-cli dependency if: needs.changes.outputs.skip_tests_dev_to_main != 'true' - run: hatch run pip install "specfact-cli==0.46.2" + run: hatch run pip install -e ./specfact-cli + - name: Runtime discovery smoke across package managers + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && matrix.python-version == '3.12' + run: | + python -m pip install pipx uv "virtualenv<21" + hatch run python specfact-cli/scripts/runtime_discovery_smoke.py --modules-repo "${GITHUB_WORKSPACE}" --launcher direct --launcher hatch-source --launcher pip-editable --launcher pipx --launcher uv-run --launcher uvx + - name: Validate generated command overview + if: needs.changes.outputs.skip_tests_dev_to_main != 'true' + run: | + hatch run check-command-overview + hatch run check-command-contract - name: Format if: needs.changes.outputs.skip_tests_dev_to_main != 'true' run: hatch run format @@ -158,10 +201,10 @@ jobs: run: hatch run check-bundle-imports - name: Contract Test if: needs.changes.outputs.skip_tests_dev_to_main != 'true' - run: hatch run contract-test - - name: Smart Test + run: hatch run contract-test-contracts + - name: Smart Test Configuration if: needs.changes.outputs.skip_tests_dev_to_main != 'true' - run: hatch run smart-test - - name: Test + run: hatch run smart-test-check + - name: Full Test Suite if: needs.changes.outputs.skip_tests_dev_to_main != 'true' run: hatch run test diff --git a/README.md b/README.md index cb03ec7a..883e730a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Central module registry for SpecFact CLI. This repository hosts official **nold-ai** bundles and their documentation. +## Command Overview + +- [Generated module command overview for humans](docs/reference/commands.generated.md) +- [AI-agent module command overview](llms.txt) + ## Highlight: AI-shaped bloat detection The Code Review bundle now surfaces `ai_bloat` findings: advisory, score-neutral signals tuned for the bloated shapes AI-assisted code commonly produces, such as identity `try/except`, one-call wrappers, passthrough lambdas, redundant intermediates, and long linear functions. A dry run on this change's affected package sources found 144 advisory candidates and applied 0 automatic rewrites; use `specfact code review run --json --out .specfact/code-review.json`, then run `/specfact.08-simplify` in your AI IDE to review each simplification with per-change confirmation. See the [AI bloat quickstart](./docs/quickstart-ai-bloat.md). diff --git a/docs/adapters/azuredevops.md b/docs/adapters/azuredevops.md index 8529e34c..2e206eb6 100644 --- a/docs/adapters/azuredevops.md +++ b/docs/adapters/azuredevops.md @@ -67,7 +67,7 @@ The adapter automatically derives work item type from your project's process tem You can override with `--ado-work-item-type`: ```bash -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --ado-work-item-type "Bug" \ @@ -437,7 +437,7 @@ This handles cases where: ```bash # Export OpenSpec change proposals to Azure DevOps work items -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --repo /path/to/openspec-repo @@ -447,7 +447,7 @@ specfact sync bridge --adapter ado --mode export-only \ ```bash # Import work items AND export proposals -specfact sync bridge --adapter ado --bidirectional \ +specfact project sync bridge --adapter ado --bidirectional \ --ado-org your-org \ --ado-project your-project \ --repo /path/to/openspec-repo @@ -457,7 +457,7 @@ specfact sync bridge --adapter ado --bidirectional \ ```bash # Import specific work items into bundle -specfact sync bridge --adapter ado --mode bidirectional \ +specfact project sync bridge --adapter ado --mode bidirectional \ --ado-org your-org \ --ado-project your-project \ --bundle main \ @@ -469,7 +469,7 @@ specfact sync bridge --adapter ado --mode bidirectional \ ```bash # Update existing work item with latest proposal content -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --change-ids add-feature-x \ @@ -481,7 +481,7 @@ specfact sync bridge --adapter ado --mode export-only \ ```bash # Detect code changes and add progress comments -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --track-code-changes \ @@ -493,7 +493,7 @@ specfact sync bridge --adapter ado --mode export-only \ ```bash # Export from bundle to ADO (uses stored lossless content) -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --bundle main \ diff --git a/docs/adapters/github.md b/docs/adapters/github.md index d9acf7a0..d61ccd23 100644 --- a/docs/adapters/github.md +++ b/docs/adapters/github.md @@ -337,14 +337,14 @@ To create a GitHub issue from an OpenSpec change and have the issue number/URL w ```bash # Export one or more changes; creates issues and updates proposal.md Source Tracking -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo . \ --repo-owner nold-ai \ --repo-name specfact-cli \ --change-ids # Example: export backlog-scrum-05-summarize-markdown-output -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo . \ --repo-owner nold-ai \ --repo-name specfact-cli \ @@ -365,7 +365,7 @@ When you improve comment logic or branch detection, use `--include-archived` to ```bash # Update all archived proposals with new comment logic -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --include-archived \ @@ -373,7 +373,7 @@ specfact sync bridge --adapter github --mode export-only \ --repo /path/to/openspec-repo # Update specific archived proposal -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --change-ids add-code-change-tracking \ diff --git a/docs/agent-rules/20-repository-context.md b/docs/agent-rules/20-repository-context.md index 2cce7f08..ce450ef4 100644 --- a/docs/agent-rules/20-repository-context.md +++ b/docs/agent-rules/20-repository-context.md @@ -51,7 +51,7 @@ hatch run contract-test hatch run smart-test hatch run test # manual code review: always include --bug-hunt (CrossHair longer budgets; see bundle docs) -hatch run specfact code review run --bug-hunt --json --out .specfact/code-review.json +hatch run specfact code review run --enforcement changed --bug-hunt --json --out .specfact/code-review.json ``` ## Architecture diff --git a/docs/agent-rules/50-quality-gates-and-review.md b/docs/agent-rules/50-quality-gates-and-review.md index d5fbc0f9..6fda0077 100644 --- a/docs/agent-rules/50-quality-gates-and-review.md +++ b/docs/agent-rules/50-quality-gates-and-review.md @@ -49,7 +49,7 @@ Before running quality gates in a fresh worktree, bootstrap the Hatch environmen 7. `hatch run contract-test` 8. `hatch run smart-test` 9. `hatch run test` -10. `hatch run specfact code review run --bug-hunt --json --out .specfact/code-review.json` (always pass **`--bug-hunt`** on manual runs so CrossHair uses bug-hunt timeouts; full-repo scope when required: add **`--scope full`**; machine-readable evidence lives at `.specfact/code-review.json` and unresolved findings block merge unless an explicit exception is documented) +10. `hatch run specfact code review run --enforcement changed --bug-hunt --json --out .specfact/code-review.json` (always pass **`--bug-hunt`** on manual runs so CrossHair uses bug-hunt timeouts; use **`--enforcement full`** when legacy blockers in reviewed files must fail the run; full-repo scope when required: add **`--scope full`**; machine-readable evidence lives at `.specfact/code-review.json` and unresolved changed-line findings block merge unless an explicit exception is documented) ## Pre-commit order @@ -68,7 +68,7 @@ Run the full pipeline manually with `./scripts/pre-commit-quality-checks.sh` or ## Clean-code review gate -The repository enforces the clean-code charter through `specfact code review run`. When agents or developers invoke the review manually (outside the pre-commit helper), include **`--bug-hunt`** so the contract runner gives CrossHair the longer bug-hunt budgets documented in the code-review bundle. Zero regressions in `naming`, `kiss`, `yagni`, `dry`, and `solid` are required before merge. +The repository enforces the clean-code charter through `specfact code review run`. When agents or developers invoke the review manually (outside the pre-commit helper), include **`--enforcement changed --bug-hunt`** so changed-line regressions block and CrossHair gets the longer bug-hunt budgets documented in the code-review bundle. Use **`--enforcement full`** only when the task explicitly requires every legacy blocker in reviewed files to fail the run. Zero regressions in `naming`, `kiss`, `yagni`, `dry`, and `solid` are required before merge. ## Module signature gate diff --git a/docs/bundles/backlog/refinement.md b/docs/bundles/backlog/refinement.md index 4c68b82b..79a07670 100644 --- a/docs/bundles/backlog/refinement.md +++ b/docs/bundles/backlog/refinement.md @@ -558,7 +558,7 @@ The most common workflow is to refine backlog items and then sync them to extern **Workflow**: `backlog ceremony refinement` → `sync bridge` 1. **Refine Backlog Items**: Use `specfact backlog ceremony refinement` to standardize backlog items with templates -2. **Sync to External Tools**: Use `specfact sync bridge` to sync refined items back to backlog tools (GitHub, ADO, etc.) +2. **Sync to External Tools**: Use `specfact project sync bridge` to sync refined items back to backlog tools (GitHub, ADO, etc.) ```bash # Complete command chaining workflow @@ -570,7 +570,7 @@ specfact backlog ceremony refinement github \ --state open # 2. Sync refined items to external tool (same or different adapter) -specfact sync bridge --adapter github \ +specfact project sync bridge --adapter github \ --repo-owner my-org --repo-name my-repo \ --backlog-ids 123,456 \ --mode export-only @@ -581,7 +581,7 @@ specfact backlog ceremony refinement github \ --write \ --labels feature -specfact sync bridge --adapter ado \ +specfact project sync bridge --adapter ado \ --ado-org my-org --ado-project my-project \ --backlog-ids 123,456 \ --mode export-only @@ -617,12 +617,12 @@ When syncing backlog items between different adapters (e.g., GitHub ↔ ADO), Sp ```bash # 1. Import closed GitHub issues into bundle (state "closed" is preserved) -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner nold-ai --repo-name specfact-cli \ --backlog-ids 110,122 # 2. Export to ADO (state is automatically mapped: closed → Closed) -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org dominikusnold --ado-project "SpecFact CLI" \ --bundle cross-sync-test --change-ids add-ado-backlog-adapter,add-template-driven-backlog-refinement @@ -649,14 +649,14 @@ specfact sync bridge --adapter ado --mode export-only \ Backlog refinement works seamlessly with the [DevOps Adapter Integration](/integrations/devops-adapter-overview/): -1. **Import Backlog Items**: Use `specfact sync bridge` to import backlog items as OpenSpec proposals +1. **Import Backlog Items**: Use `specfact project sync bridge` to import backlog items as OpenSpec proposals 2. **Refine Items**: Use `specfact backlog ceremony refinement` to standardize imported items -3. **Export Refined Items**: Use `specfact sync bridge` to export refined proposals back to backlog tools +3. **Export Refined Items**: Use `specfact project sync bridge` to export refined proposals back to backlog tools ```bash # Complete workflow # 1. Import GitHub issues as OpenSpec proposals -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner my-org --repo-name my-repo \ --backlog-ids 123,456 @@ -665,7 +665,7 @@ specfact backlog ceremony refinement github --bundle my-project --auto-bundle \ --search "is:open" # 3. Export refined proposals back to GitHub -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --bundle my-project --change-ids ``` @@ -940,11 +940,11 @@ If adapter search methods are not available: # "Note: GitHub issue fetching requires adapter.search_issues() implementation" ``` -**Workaround**: Use `specfact sync bridge` to import backlog items first, then refine: +**Workaround**: Use `specfact project sync bridge` to import backlog items first, then refine: ```bash # 1. Import backlog items -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --backlog-ids 123,456 # 2. Refine imported items from bundle diff --git a/docs/bundles/code-review/run.md b/docs/bundles/code-review/run.md index bc14db06..f6aaa796 100644 --- a/docs/bundles/code-review/run.md +++ b/docs/bundles/code-review/run.md @@ -30,7 +30,8 @@ The pipeline reviews **`.py`** and **`.pyi`** only. The **`--focus docs`** facet | `--path ` | Narrow auto-discovered review files to one or more repo-relative prefixes | | `--include-tests`, `--exclude-tests` | Control whether changed test files participate in auto-scope review | | `--focus ` | Limit auto-discovered scope to **`source`**, **`tests`**, **`docs`**, and/or **`simplify`** (repeatable); mutually exclusive with `--include-tests` / `--exclude-tests` | -| `--mode shadow\|enforce` | **`shadow`** surfaces findings without failing the exit code for policy violations; **`enforce`** applies normal gating (default **`enforce`**) | +| `--enforcement full\|changed\|shadow` | **`full`** blocks on any blocking finding in reviewed files; **`changed`** blocks only blocking findings on changed lines (default **`changed`**); **`shadow`** records evidence and never blocks | +| `--mode shadow\|enforce` | Deprecated compatibility alias: **`--mode enforce`** maps to **`--enforcement full`** and **`--mode shadow`** maps to **`--enforcement shadow`** | | `--level error\|warning` | Optional reporting level override before scoring: **`error`** keeps errors only (drops warnings and info); **`warning`** keeps errors and warnings (drops info only); omit to keep all severities (JSON, verdict, and `ci_exit_code` use the filtered list) | | `--bug-hunt` | Enable exploratory / bug-hunt style heuristics in the review pipeline | | `--include-noise`, `--suppress-noise` | Keep or suppress known low-signal findings | @@ -70,7 +71,7 @@ The Typer entrypoint validates **review flags** first: it raises **`typer.BadPar specfact code review run --scope changed # Same, with bug-hunt heuristics on the discovered file set -specfact code review run --scope changed --bug-hunt +specfact code review run --scope changed --enforcement changed --bug-hunt # Full index, limited to one package (repeat --path for more repo-relative prefixes) specfact code review run --scope full --path packages/specfact-code-review @@ -82,15 +83,21 @@ specfact code review run --scope full --path packages/specfact-code-review --pat specfact code review run --scope changed --level error # Longer CrossHair budgets for exploratory bug-hunt pass (with explicit files) -specfact code review run --bug-hunt --json --out /tmp/review-bughunt.json packages/specfact-code-review/src/specfact_code_review/run/commands.py +specfact code review run --enforcement changed --bug-hunt --json --out /tmp/review-bughunt.json packages/specfact-code-review/src/specfact_code_review/run/commands.py ``` -### Shadow mode and JSON to a file +### Enforcement modes and JSON to a file -**`--mode shadow`** runs the full toolchain but forces process exit code **`0`** and JSON **`ci_exit_code`** **`0`** so callers can ingest reports without failing a step; **`overall_verdict`** still reflects the real outcome. +**`--enforcement changed`** is the default for the CLI: it writes every finding to JSON, but only blocking findings on changed lines fail the process. Legacy findings elsewhere in touched files remain visible in **`findings`** plus **`enforcement_summary`** evidence. + +**`--enforcement full`** is the strictest mode: any blocking finding in the reviewed files fails the process, including existing issues on untouched lines. + +**`--enforcement shadow`** runs the full toolchain but forces process exit code **`0`** and JSON **`ci_exit_code`** **`0`** so callers can ingest reports without failing a step; **`overall_verdict`** still reflects the real outcome. The older **`--mode shadow`** form remains available as a compatibility alias. ```bash -specfact code review run --scope changed --mode shadow --json --out /tmp/review-report.json +specfact code review run --scope changed --enforcement changed --json --out /tmp/review-report.json +specfact code review run --scope full --enforcement full --json --out /tmp/review-full.json +specfact code review run --scope changed --enforcement shadow --json --out /tmp/review-shadow.json ``` ### `--focus` facets (repeatable) @@ -101,8 +108,8 @@ Use **`--focus`** with **`source`**, **`tests`**, **`docs`**, and/or **`simplify specfact code review run --scope changed --focus tests specfact code review run --scope full --path packages/specfact-code-review --focus source specfact code review run --scope full --focus docs -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review.json -specfact code review run --scope changed --focus simplify --with-mutation --json --out .specfact/code-review.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review.json +specfact code review run --scope changed --enforcement shadow --focus simplify --with-mutation --json --out .specfact/code-review.json ``` Use the canonical `.specfact/code-review.json` path unless every consumer in your workflow has been updated to read a custom simplify report path. @@ -145,7 +152,7 @@ The built-in `specfact/ai-bloat-patterns` policy pack is parallel to `specfact/c Use `--focus simplify` when producing the IDE simplification queue: ```bash -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review.json ``` Simplify-focused reports keep advisory `ai_bloat` findings plus high-confidence `dry` and `kiss` findings that include deterministic simplification metadata. Metadata fields such as `rewrite_hint`, `canonical_pattern`, `intent_key`, `estimated_deletion_lines`, `related_locations`, `signal_trace`, `preserve_reasons`, and `remediation_packet` are additive; legacy consumers can keep reading the original finding fields. The report-level `cleanup_forecast` summarizes reviewed LOC, estimated deletion ranges, guidance-kind totals, normalized AI-bloat density, weighted bloat points, and cleanup-yield LOC per KLOC. Simplification findings remain score-neutral; enforce mode blocks only unresolved safe-mechanical cleanup candidates. diff --git a/docs/bundles/project/import-migration.md b/docs/bundles/project/import-migration.md index c7ea379d..3fb9ae2f 100644 --- a/docs/bundles/project/import-migration.md +++ b/docs/bundles/project/import-migration.md @@ -86,10 +86,10 @@ When you restart an import on an existing bundle, the command automatically vali ```bash # First import -specfact code import my-project --repo . +specfact code import --repo . my-project # Later, restart import (validates existing features automatically) -specfact code import my-project --repo . +specfact code import --repo . my-project ``` ### Validation Results @@ -138,7 +138,7 @@ Features are saved immediately after the initial codebase analysis, before expen ```bash # Start import -specfact code import my-project --repo . +specfact code import --repo . my-project # Output shows: # ✓ Found 3156 features @@ -146,7 +146,7 @@ specfact code import my-project --repo . # ✓ Features saved (can resume if interrupted) # If you press Ctrl+C during source linking, you can restart: -specfact code import my-project --repo . +specfact code import --repo . my-project # The command will detect existing features and resume from checkpoint ``` @@ -191,7 +191,7 @@ Use `--revalidate-features` to force re-analysis even if source files haven't ch ```bash # Re-analyze all features even if files unchanged -specfact code import my-project --repo . --revalidate-features +specfact code import --repo . my-project --revalidate-features # Output shows: # ⚠ --revalidate-features enabled: Will re-analyze features even if files unchanged diff --git a/docs/bundles/project/overview.md b/docs/bundles/project/overview.md index 36736025..1bae77d3 100644 --- a/docs/bundles/project/overview.md +++ b/docs/bundles/project/overview.md @@ -20,7 +20,7 @@ The **Project** bundle (`nold-ai/specfact-project`) manages SpecFact **project b ## Command families -The project lifecycle surface loads from this bundle across **`specfact project`**, **`specfact plan`**, top-level **`specfact sync`**, and **`specfact migrate`** (see each group’s `--help` after install). The `project` group also nests a `sync` Typer; prefer the **top-level** `specfact sync …` entry when documented in the command reference. +The project lifecycle surface loads from this bundle under **`specfact project`** and **`specfact project sync`**. Use each command's `--help` output after install for option-level details. ### `specfact project` — bundles and personas @@ -39,26 +39,19 @@ The project lifecycle surface loads from this bundle across **`specfact project` | `merge` | Three-way merge with persona-aware conflict handling | | `resolve-conflict` | Resolve a specific merge conflict | | `version` | Subcommands for bundle versioning | -| `sync` | Same sync Typer as top-level `specfact sync` (see below) | +| `sync` | Same sync Typer as top-level `specfact project sync` (see below) | -### `specfact plan` — plans, stories, and reviews +### `specfact project version` — bundle versions | Command | Purpose | |--------|---------| -| `init` | Initialize or adopt a development plan for a bundle | -| `add-feature` / `add-story` | Add plan items | -| `update-idea` / `update-feature` / `update-story` | Update plan content | -| `compare` | Compare manual vs automatic plan inputs | -| `select` | Select active plan context | -| `upgrade` | Upgrade plan artifacts | -| `sync` | Sync plan artifacts with repo state | -| `promote` | Promotion workflow for plan readiness | -| `review` | Plan review workflows | -| `harden` | Harden SDD and related artifacts | +| `check` | Verify bundle version metadata | +| `bump` | Increment bundle version metadata | +| `set` | Set bundle version metadata explicitly | -### `specfact sync` — bridges and automation +### `specfact project sync` — bridges and automation -Use the top-level group (`specfact sync --help`). +Use the top-level group (`specfact project sync --help`). | Command | Purpose | |--------|---------| @@ -66,17 +59,9 @@ Use the top-level group (`specfact sync --help`). | `repository` | Repository-scoped sync operations | | `intelligent` | Higher-level orchestrated sync | -### `specfact migrate` — structure migrations - -| Command | Purpose | -|--------|---------| -| `cleanup-legacy` | Remove empty legacy top-level directories under `.specfact/` | -| `to-contracts` | Migrate verbose bundles toward contract-oriented layouts | -| `artifacts` | Migrate plan and artifact layouts | - ## Related: codebase import -Brownfield **code import** (`specfact code import`, `specfact import …`) lives in the [Codebase](/bundles/codebase/overview/) bundle; it often feeds project bundles. See [Import command features](../import-migration/) for behavior that spans both bundles. +Brownfield **code import** (`specfact code import from-code`, `specfact code import from-bridge`) lives in the [Codebase](/bundles/codebase/overview/) bundle; it often feeds project bundles. See [Import command features](../import-migration/) for behavior that spans both bundles. ## Bundle-owned prompts and plan templates @@ -88,9 +73,9 @@ The project prompt set includes `/specfact.08-simplify`, which reads `.specfact/ ```bash specfact project link-backlog --adapter github --project-id owner/repo --bundle my-bundle --repo . -specfact plan init --help -specfact sync bridge --help -specfact migrate artifacts --repo . +specfact project health-check --project-name my-bundle --repo . +specfact project sync bridge --help +specfact project version check --bundle my-bundle --repo . ``` ## See also diff --git a/docs/bundles/spec/validate.md b/docs/bundles/spec/validate.md index 1210448f..2446b391 100644 --- a/docs/bundles/spec/validate.md +++ b/docs/bundles/spec/validate.md @@ -24,7 +24,7 @@ Use the Spec bundle to validate OpenAPI or AsyncAPI contracts with Specmatic and - Validates one contract file or every contract in a selected bundle. - Uses Specmatic for schema checks and example validation. -- Supports bundle-driven validation with the active plan from `specfact plan select`. +- Supports bundle-driven validation with the active plan from `specfact project --help`. - Caches validation results in `.specfact/cache/specmatic-validation.json` unless you pass `--force`. ## Key options for `specfact spec validate` diff --git a/docs/getting-started/choose-your-modules.md b/docs/getting-started/choose-your-modules.md index afe8358e..c238254a 100644 --- a/docs/getting-started/choose-your-modules.md +++ b/docs/getting-started/choose-your-modules.md @@ -20,7 +20,7 @@ SpecFact ships as six independent modules. Each solves a distinct problem in you | I need to... | Install this | Command surface | |---|---|---| | Manage my backlog with template-driven refinement, standups, and sprint planning | **Backlog** | `specfact backlog` | -| Structure my project, link plans to code, and manage dev lifecycle | **Project** | `specfact project`, `specfact plan`, `specfact sync` | +| Structure my project, link plans to code, and manage dev lifecycle | **Project** | `specfact project`, `specfact project`, `specfact project sync` | | Import a legacy codebase, detect features, and track spec drift | **Codebase** | `specfact code import`, `specfact code drift` | | Run governed code reviews with quality scoring and house rules | **Code Review** | `specfact code review` | | Validate OpenAPI/AsyncAPI contracts and generate test suites | **Spec** | `specfact spec validate`, `specfact spec mock` | @@ -250,7 +250,7 @@ For intermediate and advanced users — each module provides configuration hooks | What | How | Use case | |---|---|---| -| **Sync bridges** | `specfact sync bridge` config | Connect to GitHub, Linear, Jira, OpenSpec, Spec-Kit | +| **Sync bridges** | `specfact project sync bridge` config | Connect to GitHub, Linear, Jira, OpenSpec, Spec-Kit | | **Personas** | `specfact project init-personas` | Map team roles to project bundle access | | **Import entry points** | `specfact code import --entry-point` | Specify monorepo roots for brownfield analysis | | **DevOps stages** | Stage action configuration | Customize the plan → develop → review → release flow | diff --git a/docs/getting-started/first-steps.md b/docs/getting-started/first-steps.md index 70cb7a8b..ec375e34 100644 --- a/docs/getting-started/first-steps.md +++ b/docs/getting-started/first-steps.md @@ -71,12 +71,12 @@ cd your-project/ specfact code review run ``` -This analyzes your code with Ruff, Semgrep, Pylint, BasedPyright, Radon, and CrossHair, then produces a quality score. No configuration needed. +This analyzes your code with Ruff, Semgrep, Pylint, BasedPyright, Radon, and CrossHair, then produces a quality score. The default CLI enforcement is **changed**: blocking findings on changed lines fail the run, while older findings elsewhere stay in the report as evidence. Use `--enforcement full` for strict whole-file blocking or `--enforcement shadow` for evidence-only reports. To review only changed files: ```bash -specfact code review run --scope changed +specfact code review run --scope changed --enforcement changed ``` Check your quality ledger over time: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index cd12e0b3..2217176a 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -283,12 +283,12 @@ Convert an existing GitHub Spec-Kit project: ```bash # Start a one-time import -specfact sync bridge \ +specfact project sync bridge \ --adapter speckit \ --repo ./my-speckit-project # Ongoing bidirectional sync (after migration) -specfact sync bridge --adapter speckit --bundle --repo . --bidirectional --watch +specfact project sync bridge --adapter speckit --bundle --repo . --bidirectional --watch ``` **Bidirectional Sync:** @@ -297,13 +297,13 @@ Keep Spec-Kit and SpecFact artifacts synchronized: ```bash # One-time sync -specfact sync bridge --adapter speckit --bundle --repo . --bidirectional +specfact project sync bridge --adapter speckit --bundle --repo . --bidirectional # Continuous watch mode -specfact sync bridge --adapter speckit --bundle --repo . --bidirectional --watch +specfact project sync bridge --adapter speckit --bundle --repo . --bidirectional --watch ``` -**Note**: SpecFact CLI uses a plugin-based adapter registry pattern. All adapters (Spec-Kit, OpenSpec, GitHub, etc.) are registered in `AdapterRegistry` and accessed via `specfact sync bridge --adapter `, making the architecture extensible for future tool integrations. +**Note**: SpecFact CLI uses a plugin-based adapter registry pattern. All adapters (Spec-Kit, OpenSpec, GitHub, etc.) are registered in `AdapterRegistry` and accessed via `specfact project sync bridge --adapter `, making the architecture extensible for future tool integrations. ### For Brownfield Projects diff --git a/docs/guides/ai-ide-workflow.md b/docs/guides/ai-ide-workflow.md index d291a372..6ab21f7a 100644 --- a/docs/guides/ai-ide-workflow.md +++ b/docs/guides/ai-ide-workflow.md @@ -42,8 +42,8 @@ Examples: ```bash specfact backlog ceremony refinement github --preview --labels feature -specfact code review run src --scope changed --no-tests -specfact sync bridge --adapter github --mode export-only --repo . --bundle legacy-api +specfact code review run --scope changed --path src --enforcement shadow --no-tests +specfact project sync bridge --adapter github --mode export-only --repo . --bundle legacy-api ``` These commands are the source of truth. The IDE should support them, not replace them. diff --git a/docs/guides/brownfield-examples.md b/docs/guides/brownfield-examples.md index 99cebd30..f7863a29 100644 --- a/docs/guides/brownfield-examples.md +++ b/docs/guides/brownfield-examples.md @@ -16,7 +16,7 @@ These examples give you three concrete modernization patterns you can adapt with Use this when an undocumented repository needs a bundle baseline before any release work: ```bash -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api specfact code analyze contracts --repo . --bundle legacy-api specfact spec validate --bundle legacy-api ``` @@ -30,7 +30,7 @@ Use this when backlog items must be refined before the modernization work is syn ```bash specfact backlog ceremony refinement github --preview --labels feature specfact backlog verify-readiness --adapter github --project-id owner/repo --target-items 123 -specfact sync bridge --adapter github --mode export-only --repo . --bundle legacy-api +specfact project sync bridge --adapter github --mode export-only --repo . --bundle legacy-api ``` Outcome: backlog items are standardized before they drive bundle changes. diff --git a/docs/guides/brownfield-faq-and-roi.md b/docs/guides/brownfield-faq-and-roi.md index f63ab63c..375367bf 100644 --- a/docs/guides/brownfield-faq-and-roi.md +++ b/docs/guides/brownfield-faq-and-roi.md @@ -18,7 +18,7 @@ Start with a project bundle plus IDE resource bootstrap: ```bash specfact init --profile solo-developer specfact init ide --repo . --ide cursor -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api ``` That gives you a repeatable baseline without needing to modernize the whole codebase at once. diff --git a/docs/guides/brownfield-modernization.md b/docs/guides/brownfield-modernization.md index 4d837bff..01582bd3 100644 --- a/docs/guides/brownfield-modernization.md +++ b/docs/guides/brownfield-modernization.md @@ -23,7 +23,7 @@ The IDE bootstrap matters because backlog refinement, review prompts, and other ## 2. Import the legacy codebase into a project bundle ```bash -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api ``` This creates or refreshes the project bundle that the later workflow stages use. @@ -39,7 +39,7 @@ Use this to identify where the codebase already has contract signals and where m ## 4. Sync or export project state when outside tools are involved ```bash -specfact sync bridge --adapter github --mode export-only --repo . --bundle legacy-api +specfact project sync bridge --adapter github --mode export-only --repo . --bundle legacy-api ``` Use the bridge layer when you need to exchange bundle state with GitHub, Azure DevOps, OpenSpec, or another supported adapter. diff --git a/docs/guides/ci-cd-pipeline.md b/docs/guides/ci-cd-pipeline.md index 17e05783..81824710 100644 --- a/docs/guides/ci-cd-pipeline.md +++ b/docs/guides/ci-cd-pipeline.md @@ -62,7 +62,7 @@ GitHub Actions also runs: ## 3. Add scoped workflow checks while developing ```bash -specfact code review run docs/guides/cross-module-chains.md --no-tests +specfact code review run --enforcement changed docs/guides/cross-module-chains.md --no-tests specfact govern enforce sdd legacy-api --no-interactive ``` diff --git a/docs/guides/command-chains.md b/docs/guides/command-chains.md index a354dbfa..a5b9d765 100644 --- a/docs/guides/command-chains.md +++ b/docs/guides/command-chains.md @@ -26,7 +26,7 @@ Related: [AI IDE workflow](/ai-ide-workflow/) ## 2. Brownfield intake and contract discovery ```bash -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api specfact code analyze contracts --repo . --bundle legacy-api specfact spec validate --bundle legacy-api --force ``` @@ -40,7 +40,7 @@ Related: [Brownfield modernization](/guides/brownfield-modernization/) ```bash specfact backlog ceremony refinement github --preview --labels feature specfact backlog verify-readiness --adapter github --project-id owner/repo --target-items 123 -specfact sync bridge --adapter github --mode export-only --repo . --bundle legacy-api +specfact project sync bridge --adapter github --mode export-only --repo . --bundle legacy-api ``` Use this chain when backlog items must be standardized and readiness-checked before you export or sync them into project artifacts. @@ -63,7 +63,7 @@ Related: [Contract testing workflow](/contract-testing-workflow/) ```bash specfact backlog ceremony standup github -specfact code review run docs/guides/cross-module-chains.md --no-tests +specfact code review run --enforcement changed docs/guides/cross-module-chains.md --no-tests specfact govern enforce sdd legacy-api --no-interactive ``` diff --git a/docs/guides/cross-module-chains.md b/docs/guides/cross-module-chains.md index e0c16a6f..a297f605 100644 --- a/docs/guides/cross-module-chains.md +++ b/docs/guides/cross-module-chains.md @@ -27,7 +27,7 @@ specfact init ide --repo . --ide cursor ```bash specfact backlog ceremony refinement github --preview --labels feature specfact backlog verify-readiness --adapter github --project-id owner/repo --target-items 123 -specfact sync bridge --adapter github --mode export-only --repo . --bundle legacy-api +specfact project sync bridge --adapter github --mode export-only --repo . --bundle legacy-api ``` Use this chain when work starts in an external backlog tool and must be cleaned up before it becomes a SpecFact project artifact. @@ -35,7 +35,7 @@ Use this chain when work starts in an external backlog tool and must be cleaned ## Chain 2. Brownfield intake -> contract validation ```bash -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api specfact code analyze contracts --repo . --bundle legacy-api specfact spec validate --bundle legacy-api --force ``` @@ -55,7 +55,7 @@ Use this chain when a contract changed and you want compatibility checks, genera ## Chain 4. Review loop for changed files ```bash -specfact code review run src --scope changed --no-tests +specfact code review run --scope changed --path src --enforcement changed --no-tests specfact govern enforce stage --preset balanced specfact govern enforce sdd legacy-api --no-interactive ``` diff --git a/docs/guides/daily-devops-routine.md b/docs/guides/daily-devops-routine.md index 02e02aa0..c6f0585c 100644 --- a/docs/guides/daily-devops-routine.md +++ b/docs/guides/daily-devops-routine.md @@ -36,7 +36,7 @@ Reference: [Cross-module chains](/guides/cross-module-chains/) ```bash specfact init ide --repo . --ide cursor -specfact code import legacy-api --repo . +specfact code import --repo . legacy-api ``` Refresh IDE resources when the workflow depends on installed prompts, then import or refresh the project bundle before deeper validation. @@ -46,7 +46,7 @@ Reference: [AI IDE workflow](/ai-ide-workflow/) ## 4. Midday quality review ```bash -specfact code review run src --scope changed --no-tests +specfact code review run --scope changed --path src --enforcement changed --no-tests specfact spec validate --bundle legacy-api ``` diff --git a/docs/guides/integrations-overview.md b/docs/guides/integrations-overview.md index 9f861fe7..c0762448 100644 --- a/docs/guides/integrations-overview.md +++ b/docs/guides/integrations-overview.md @@ -45,7 +45,7 @@ SpecFact CLI integrations fall into four main categories: - ✅ Rapid prototyping workflow: constitution → specify → clarify → plan → tasks → analyze → implement - ✅ Constitution and planning for new features - ✅ IDE integration with GitHub Copilot chat and other supported agents -- ✅ Bridge export from Spec-Kit feature folders into OpenSpec change proposals with `specfact sync bridge --adapter speckit --mode change-proposal` +- ✅ Bridge export from Spec-Kit feature folders into OpenSpec change proposals with `specfact project sync bridge --adapter speckit --mode change-proposal` **When to use**: @@ -60,10 +60,10 @@ SpecFact CLI integrations fall into four main categories: ```bash # Convert one Spec-Kit feature into an OpenSpec change proposal -specfact sync bridge --adapter speckit --repo . --mode change-proposal --feature 001-auth-sync +specfact project sync bridge --adapter speckit --repo . --mode change-proposal --feature 001-auth-sync # Convert every untracked Spec-Kit feature into OpenSpec changes -specfact sync bridge --adapter speckit --repo . --mode change-proposal --all +specfact project sync bridge --adapter speckit --repo . --mode change-proposal --all ``` **See also**: [Spec-Kit Journey Guide](./speckit-journey.md) diff --git a/docs/guides/openspec-journey.md b/docs/guides/openspec-journey.md index a72132db..77ba046f 100644 --- a/docs/guides/openspec-journey.md +++ b/docs/guides/openspec-journey.md @@ -147,7 +147,7 @@ Add new feature X to improve user experience. EOF # Step 2: Export to GitHub Issues -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --repo /path/to/openspec-repo @@ -170,7 +170,7 @@ sequenceDiagram participant GH as GitHub Issues Dev->>OS: Create change proposal
openspec/changes/add-feature-x/ - Dev->>SF: specfact sync bridge --adapter github + Dev->>SF: specfact project sync bridge --adapter github SF->>OS: Read proposal.md SF->>GH: Create issue from proposal GH-->>SF: Issue #123 created @@ -179,7 +179,7 @@ sequenceDiagram Note over Dev,GH: Implementation Phase Dev->>Dev: Make commits with change ID - Dev->>SF: specfact sync bridge --track-code-changes + Dev->>SF: specfact project sync bridge --track-code-changes SF->>SF: Detect commits mentioning
change ID SF->>GH: Add progress comment
to issue #123 GH-->>Dev: Progress visible in issue @@ -211,7 +211,7 @@ Read-only sync from OpenSpec to SpecFact for change proposal tracking: ```bash # Sync OpenSpec change proposals to SpecFact -specfact sync bridge --adapter openspec --mode read-only \ +specfact project sync bridge --adapter openspec --mode read-only \ --bundle my-project \ --repo /path/to/openspec-repo @@ -267,7 +267,7 @@ Full bidirectional sync between OpenSpec and SpecFact: ```bash # Bidirectional sync (future) -specfact sync bridge --adapter openspec --bidirectional \ +specfact project sync bridge --adapter openspec --bidirectional \ --bundle my-project \ --repo /path/to/openspec-repo \ --watch @@ -315,7 +315,7 @@ Here's how to use both tools together for legacy code modernization: ```bash # Step 1: Analyze legacy code with SpecFact -specfact code import legacy-api --repo ./legacy-app +specfact code import --repo ./legacy-app legacy-api # → Extracts features from existing code # → Creates SpecFact bundle: .specfact/projects/legacy-api/ @@ -338,7 +338,7 @@ Legacy API needs modernization for better performance and maintainability. EOF # Step 3: Export proposal to GitHub Issues ✅ IMPLEMENTED -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --repo /path/to/openspec-repo @@ -347,7 +347,7 @@ specfact sync bridge --adapter github --mode export-only \ git commit -m "feat: modernize-api - refactor endpoints" # Step 5: Track progress ✅ IMPLEMENTED -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --track-code-changes \ @@ -355,7 +355,7 @@ specfact sync bridge --adapter github --mode export-only \ --code-repo /path/to/source-code-repo # Step 6: Sync OpenSpec change proposals ✅ AVAILABLE -specfact sync bridge --adapter openspec --mode read-only \ +specfact project sync bridge --adapter openspec --mode read-only \ --bundle legacy-api \ --repo /path/to/openspec-repo # → Generates alignment report diff --git a/docs/guides/speckit-comparison.md b/docs/guides/speckit-comparison.md index 3040f150..164096e4 100644 --- a/docs/guides/speckit-comparison.md +++ b/docs/guides/speckit-comparison.md @@ -216,19 +216,19 @@ expertise_level: [intermediate] # (Interactive slash commands in GitHub) # Step 2: Convert a Spec-Kit feature into an OpenSpec change proposal -specfact sync bridge --adapter speckit --repo ./my-project --mode change-proposal --feature 001-auth-sync +specfact project sync bridge --adapter speckit --repo ./my-project --mode change-proposal --feature 001-auth-sync # Step 3: Bulk-convert every untracked Spec-Kit feature into OpenSpec changes -specfact sync bridge --adapter speckit --repo ./my-project --mode change-proposal --all +specfact project sync bridge --adapter speckit --repo ./my-project --mode change-proposal --all # Step 4: Add runtime contracts to critical Python paths # (SpecFact contract decorators) # Step 5: Keep both in sync (using adapter registry pattern) -specfact sync bridge --adapter speckit --bundle --repo . --bidirectional +specfact project sync bridge --adapter speckit --bundle --repo . --bidirectional ``` -**Note**: SpecFact CLI uses a plugin-based adapter registry pattern. All adapters (Spec-Kit, OpenSpec, GitHub, etc.) are registered in `AdapterRegistry` and accessed via `specfact sync bridge --adapter `, making the architecture extensible for future tool integrations. +**Note**: SpecFact CLI uses a plugin-based adapter registry pattern. All adapters (Spec-Kit, OpenSpec, GitHub, etc.) are registered in `AdapterRegistry` and accessed via `specfact project sync bridge --adapter `, making the architecture extensible for future tool integrations. --- @@ -300,14 +300,14 @@ Use both together for best results. - **GitHub Issues** - Export change proposals to DevOps backlogs - **Future**: Linear, Jira, Azure DevOps, and more -All adapters are registered in `AdapterRegistry` and accessed via `specfact sync bridge --adapter `, making the architecture extensible for future tool integrations. +All adapters are registered in `AdapterRegistry` and accessed via `specfact project sync bridge --adapter `, making the architecture extensible for future tool integrations. ### Can I migrate from Spec-Kit to SpecFact? **Yes.** SpecFact can import Spec-Kit artifacts: ```bash -specfact sync bridge --adapter speckit --repo ./my-project +specfact project sync bridge --adapter speckit --repo ./my-project ``` You can also keep using both tools with bidirectional sync via the adapter registry pattern. @@ -318,7 +318,7 @@ You can also keep using both tools with bidirectional sync via the adapter regis ```bash # Read-only sync from OpenSpec to SpecFact -specfact sync bridge --adapter openspec --mode read-only \ +specfact project sync bridge --adapter openspec --mode read-only \ --bundle my-project \ --repo /path/to/openspec-repo ``` diff --git a/docs/guides/speckit-journey.md b/docs/guides/speckit-journey.md index 13d668e1..b0c71000 100644 --- a/docs/guides/speckit-journey.md +++ b/docs/guides/speckit-journey.md @@ -56,13 +56,13 @@ SpecFact complements this flow in two common ways. Use this when you want SpecFact change tracking, backlog sync, or downstream governance on top of an existing Spec-Kit feature: ```bash -specfact sync bridge --adapter speckit --repo . --mode change-proposal --feature 001-auth-sync +specfact project sync bridge --adapter speckit --repo . --mode change-proposal --feature 001-auth-sync ``` To convert every untracked feature in the repository: ```bash -specfact sync bridge --adapter speckit --repo . --mode change-proposal --all +specfact project sync bridge --adapter speckit --repo . --mode change-proposal --all ``` ### 2. Add SpecFact enforcement after specification work diff --git a/docs/integrations/devops-adapter-overview.md b/docs/integrations/devops-adapter-overview.md index 300766a2..bb7214be 100644 --- a/docs/integrations/devops-adapter-overview.md +++ b/docs/integrations/devops-adapter-overview.md @@ -118,7 +118,7 @@ EOF Export the change proposal to create a GitHub issue: ```bash -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --repo /path/to/openspec-repo @@ -133,7 +133,7 @@ As you implement the feature, track progress automatically: git commit -m "feat: implement add-feature-x - initial API design" # Track progress (detects commits and adds comments) -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --track-code-changes \ @@ -179,7 +179,7 @@ specfact backlog auth github --client-id YOUR_CLIENT_ID ```bash # Uses gh auth token automatically -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --use-gh-cli @@ -189,7 +189,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash export GITHUB_TOKEN=ghp_your_token_here -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo ``` @@ -197,7 +197,7 @@ specfact sync bridge --adapter github --mode export-only \ **Option 4: Command Line Flag** ```bash -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --github-token ghp_your_token_here @@ -209,7 +209,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash # Export all active proposals to GitHub Issues -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --repo /path/to/openspec-repo @@ -219,7 +219,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash # Detect code changes and add progress comments -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --track-code-changes \ @@ -230,7 +230,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash # Export only specific change proposals -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --change-ids add-feature-x,update-api \ @@ -285,7 +285,7 @@ ado: So after authenticating once, **running from the repo root is enough** for both GitHub and ADO—org/repo or org/project are detected automatically from the git remote. -Applies to all backlog commands: `specfact backlog daily`, `specfact backlog refine`, `specfact sync bridge`, etc. +Applies to all backlog commands: `specfact backlog daily`, `specfact backlog refine`, `specfact project sync bridge`, etc. --- @@ -303,7 +303,7 @@ Applies to all backlog commands: `specfact backlog daily`, `specfact backlog ref ```bash # ✅ CORRECT: Direct export from OpenSpec to GitHub -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --change-ids add-feature-x \ @@ -333,7 +333,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash # Step 1: Import GitHub issue into bundle (stores lossless content) -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name your-repo \ --bundle migration-bundle \ --backlog-ids 123 @@ -342,7 +342,7 @@ specfact sync bridge --adapter github --mode bidirectional \ # Note the change_id from output # Step 2: Export from bundle to ADO (uses stored content) -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project your-project \ --bundle migration-bundle \ --change-ids add-feature-x # Use change_id from Step 1 @@ -366,7 +366,7 @@ specfact sync bridge --adapter ado --mode export-only \ ```bash # ❌ WRONG: This will show "0 backlog items exported" -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name your-repo \ --bundle some-bundle \ --change-ids add-feature-x \ @@ -379,7 +379,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash # ✅ CORRECT: Direct export (no --bundle) -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name your-repo \ --change-ids add-feature-x \ --repo /path/to/openspec-repo @@ -418,13 +418,13 @@ When your OpenSpec change proposals are in a different repository than your sour # Source code in specfact-cli # Step 1: Create issue from proposal -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner nold-ai \ --repo-name specfact-cli-internal \ --repo /path/to/specfact-cli-internal # Step 2: Track code changes from source code repo -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner nold-ai \ --repo-name specfact-cli-internal \ --track-code-changes \ @@ -468,7 +468,7 @@ When exporting to public repositories, use content sanitization to protect inter ```bash # Public repository: sanitize content -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name public-repo \ --sanitize \ @@ -476,7 +476,7 @@ specfact sync bridge --adapter github --mode export-only \ --repo /path/to/openspec-repo # Internal repository: use full content -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name internal-repo \ --no-sanitize \ @@ -576,7 +576,7 @@ When `--sanitize` is enabled, progress comments are sanitized: 2. **Export to GitHub**: ```bash - specfact sync bridge --adapter github --mode export-only \ + specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --repo /path/to/openspec-repo @@ -599,7 +599,7 @@ When `--sanitize` is enabled, progress comments are sanitized: 2. **Track Progress**: ```bash - specfact sync bridge --adapter github --mode export-only \ + specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --track-code-changes \ @@ -618,7 +618,7 @@ When `--sanitize` is enabled, progress comments are sanitized: Add manual progress comments without code change detection: ```bash -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --add-progress-comment \ @@ -643,7 +643,7 @@ SpecFact supports more than exporting and updating backlog items: Example: Import selected GitHub issues into a bundle and keep them in sync: ```bash -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name your-repo \ --bundle main \ --backlog-ids 111,112 @@ -677,7 +677,7 @@ Migrate a GitHub issue to Azure DevOps while preserving all content: ```bash # Step 1: Import GitHub issue into bundle (stores lossless content) # This creates a change proposal in the bundle and stores raw content -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name your-repo \ --bundle main \ --backlog-ids 123 @@ -697,7 +697,7 @@ ls /path/to/openspec-repo/openspec/changes/ # Step 3: Export from bundle to ADO (uses stored lossless content) # Replace with the actual change_id from Step 1 -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project your-project \ --bundle main \ --change-ids add-feature-x # Use the actual change_id from Step 1 @@ -756,7 +756,7 @@ Keep proposals in sync across GitHub (public) and ADO (internal): ```bash # Day 1: Create proposal in OpenSpec, export to GitHub (public) # Assume change_id is "add-feature-x" (from openspec/changes/add-feature-x/proposal.md) -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name public-repo \ --sanitize \ --repo /path/to/openspec-repo \ @@ -767,7 +767,7 @@ specfact sync bridge --adapter github --mode export-only \ # Day 2: Import GitHub issue into bundle (for internal team) # This stores lossless content in the bundle -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name public-repo \ --bundle internal \ --backlog-ids 123 @@ -777,7 +777,7 @@ specfact sync bridge --adapter github --mode bidirectional \ # Day 3: Export to ADO for internal tracking (full content, no sanitization) # Uses the change_id from Day 2 -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project internal-project \ --bundle internal \ --change-ids add-feature-x @@ -787,7 +787,7 @@ specfact sync bridge --adapter ado --mode export-only \ # Day 4: Update in ADO, sync back to GitHub (status sync) # Import ADO work item to update bundle with latest status -specfact sync bridge --adapter ado --mode bidirectional \ +specfact project sync bridge --adapter ado --mode bidirectional \ --ado-org your-org --ado-project internal-project \ --bundle internal \ --backlog-ids 456 @@ -796,7 +796,7 @@ specfact sync bridge --adapter ado --mode bidirectional \ # Bundle now has latest status from ADO # Then sync status back to GitHub -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name public-repo \ --update-existing \ --repo /path/to/openspec-repo \ @@ -858,7 +858,7 @@ export AZURE_DEVOPS_TOKEN='your-ado-token' # Step 1: Import GitHub issue into bundle # This stores the issue in a bundle with lossless content preservation -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name your-repo \ --bundle migration-bundle \ --backlog-ids 123 @@ -874,7 +874,7 @@ ls .specfact/projects/migration-bundle/change_tracking/proposals/ # Step 3: Export to Azure DevOps # Use the change_id from Step 1 -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project your-project \ --bundle migration-bundle \ --change-ids add-feature-x @@ -889,13 +889,13 @@ specfact sync bridge --adapter ado --mode export-only \ # Content should match exactly (Why, What Changes sections, formatting) # Step 5: Optional - Round-trip back to GitHub to verify -specfact sync bridge --adapter ado --mode bidirectional \ +specfact project sync bridge --adapter ado --mode bidirectional \ --ado-org your-org --ado-project your-project \ --bundle migration-bundle \ --backlog-ids 456 # Then export back to GitHub -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name your-repo \ --bundle migration-bundle \ --change-ids add-feature-x \ @@ -927,7 +927,7 @@ export AZURE_DEVOPS_TOKEN='your-ado-token' # Import GitHub issue #110 into bundle 'cross-sync-test' # Note: Bundle will be auto-created if it doesn't exist # This stores lossless content in the bundle -specfact sync bridge --adapter github --mode bidirectional \ +specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner nold-ai --repo-name specfact-cli \ --bundle cross-sync-test \ --backlog-ids 110 @@ -948,7 +948,7 @@ ls /path/to/openspec-repo/openspec/changes/ # ============================================================ # Export the proposal to ADO using the change_id from Step 1 # Replace with the actual change_id from Step 1 -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project your-project \ --bundle cross-sync-test \ --change-ids @@ -964,7 +964,7 @@ specfact sync bridge --adapter ado --mode export-only \ # Import the ADO work item back into the bundle # This updates the bundle with ADO's version of the content # Replace with the ID from Step 2 -specfact sync bridge --adapter ado --mode bidirectional \ +specfact project sync bridge --adapter ado --mode bidirectional \ --ado-org your-org --ado-project your-project \ --bundle cross-sync-test \ --backlog-ids @@ -978,7 +978,7 @@ specfact sync bridge --adapter ado --mode bidirectional \ # ============================================================ # Export back to GitHub to complete the round-trip # This updates the original GitHub issue with any changes from ADO -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner nold-ai --repo-name specfact-cli \ --bundle cross-sync-test \ --change-ids \ @@ -1062,7 +1062,7 @@ The change proposal must have `source_tracking` metadata linking it to the GitHu To update a specific change proposal's linked issue: ```bash -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --change-ids your-change-id \ @@ -1075,7 +1075,7 @@ specfact sync bridge --adapter github --mode export-only \ ```bash cd /path/to/openspec-repo -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner nold-ai \ --repo-name specfact-cli \ --change-ids implement-adapter-enhancement-recommendations \ @@ -1088,7 +1088,7 @@ specfact sync bridge --adapter github --mode export-only \ To update all change proposals that have linked GitHub issues: ```bash -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --update-existing \ @@ -1139,7 +1139,7 @@ By default, archived change proposals (in `openspec/changes/archive/`) are exclu ```bash # Update all archived proposals with new comment logic -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --include-archived \ @@ -1147,7 +1147,7 @@ specfact sync bridge --adapter github --mode export-only \ --repo /path/to/openspec-repo # Update specific archived proposal -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org \ --repo-name your-repo \ --change-ids add-code-change-tracking \ @@ -1169,7 +1169,7 @@ When `--include-archived` is used with `--update-existing`: ```bash # Update issue #107 with improved branch detection -specfact sync bridge --adapter github --mode export-only \ +specfact project sync bridge --adapter github --mode export-only \ --repo-owner nold-ai \ --repo-name specfact-cli \ --change-ids add-code-change-tracking \ @@ -1257,7 +1257,7 @@ Verify `openspec/changes//proposal.md` was updated: ```bash # ❌ WRONG: Using --bundle when exporting from OpenSpec - specfact sync bridge --adapter github --mode export-only \ + specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name your-repo \ --bundle some-bundle \ --change-ids add-feature-x \ @@ -1275,7 +1275,7 @@ Verify `openspec/changes//proposal.md` was updated: ```bash # ✅ CORRECT: Direct export from OpenSpec - specfact sync bridge --adapter github --mode export-only \ + specfact project sync bridge --adapter github --mode export-only \ --repo-owner your-org --repo-name your-repo \ --change-ids add-feature-x \ --repo /path/to/openspec-repo @@ -1285,13 +1285,13 @@ Verify `openspec/changes//proposal.md` was updated: ```bash # Step 1: Import from backlog into bundle - specfact sync bridge --adapter github --mode bidirectional \ + specfact project sync bridge --adapter github --mode bidirectional \ --repo-owner your-org --repo-name your-repo \ --bundle your-bundle \ --backlog-ids 123 # Step 2: Export from bundle (now it will work) - specfact sync bridge --adapter ado --mode export-only \ + specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org --ado-project your-project \ --bundle your-bundle \ --change-ids @@ -1452,13 +1452,13 @@ specfact backlog auth azure-devops # Option 2: Environment Variable export AZURE_DEVOPS_TOKEN=your_pat_token -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --repo /path/to/openspec-repo # Option 3: Command Line Flag -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --ado-token your_pat_token \ @@ -1469,26 +1469,26 @@ specfact sync bridge --adapter ado --mode export-only \ ```bash # Bidirectional sync (import work items AND export proposals) -specfact sync bridge --adapter ado --bidirectional \ +specfact project sync bridge --adapter ado --bidirectional \ --ado-org your-org \ --ado-project your-project \ --repo /path/to/openspec-repo # Export-only (one-way: OpenSpec → ADO) -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --repo /path/to/openspec-repo # Export with explicit work item type -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --ado-work-item-type "User Story" \ --repo /path/to/openspec-repo # Track code changes and add progress comments -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --track-code-changes \ @@ -1507,7 +1507,7 @@ The ADO adapter automatically derives work item type from your project's process You can override with `--ado-work-item-type`: ```bash -specfact sync bridge --adapter ado --mode export-only \ +specfact project sync bridge --adapter ado --mode export-only \ --ado-org your-org \ --ado-project your-project \ --ado-work-item-type "Bug" \ diff --git a/docs/modules/code-review.md b/docs/modules/code-review.md index 6693606f..408e8421 100644 --- a/docs/modules/code-review.md +++ b/docs/modules/code-review.md @@ -40,11 +40,12 @@ Options (aligned with `specfact code review run --help`): file sets, then intersect with the resolved scope. When any `--focus` is set, **`--include-tests` and `--exclude-tests` are rejected** (use focus alone to express test intent) -- `--mode shadow|enforce`: **enforce** (default) keeps today’s non-zero process - exit when the governed report says the run failed; **shadow** still runs the - full toolchain and preserves `overall_verdict` in JSON, but forces - `ci_exit_code` and the process exit code to `0` so CI or hooks can log signal - without blocking +- `--enforcement full|changed|shadow`: **changed** is the CLI/default hook mode + and blocks only blocking findings on changed lines; **full** blocks on any + blocking finding in reviewed files; **shadow** records the full report as + evidence but forces `ci_exit_code` and the process exit code to `0` +- `--mode shadow|enforce`: deprecated compatibility alias. Use + `--enforcement shadow` or `--enforcement full` in new docs and automation - `--level error|warning`: filter findings **before** scoring so JSON, tables, score, verdict, and `ci_exit_code` match the filtered list: **`error`** keeps errors only (warnings and info dropped); **`warning`** keeps errors and @@ -122,7 +123,7 @@ guide (same Typer surface as this section). The review pipeline also emits `ai_bloat` findings for code shapes commonly amplified by AI-assisted generation: manual append loops, passthrough lambdas, identity `try/except`, one-call wrappers, speculative `Optional[...] = None` parameters, duplicate terminal guards, long low-branch functions, and redundant intermediates. -These findings are `severity=info`, advisory-only, and score-neutral. They are written to `.specfact/code-review.json` when the report includes all severities; for simplification queues, write `.specfact/code-review-simplify.json` with `--focus simplify` so `/specfact.08-simplify` can filter them by `category=ai_bloat` for per-change confirmed rewrites. Simplify JSON now includes `cleanup_forecast` at report level plus per-finding `signal_trace`, `preserve_reasons`, and `remediation_packet` where available. They do not claim AI authorship; they identify simplification candidates. +These findings are `severity=info`, advisory-only, and score-neutral. They are written to `.specfact/code-review.json` when the report includes all severities; for simplification queues, write `.specfact/code-review-simplify.json` with `--enforcement shadow --focus simplify` so `/specfact.08-simplify` can filter them by `category=ai_bloat` for per-change confirmed rewrites without blocking the evidence step. Simplify JSON now includes `cleanup_forecast` at report level plus per-finding `signal_trace`, `preserve_reasons`, and `remediation_packet` where available. They do not claim AI authorship; they identify simplification candidates. For the lowest-friction AI onboarding path, start with the built-in instruction printer instead of requiring a user to install IDE prompts or skills first: @@ -155,10 +156,11 @@ findings such as: ### Exit codes -- `0`: `PASS` or `PASS_WITH_ADVISORY`, or any outcome under **`--mode shadow`** - (shadow forces success at the process level even when `overall_verdict` is - `FAIL`) -- `1`: `FAIL` under default **enforce** semantics +- `0`: `PASS` or `PASS_WITH_ADVISORY`, any outcome under + **`--enforcement shadow`**, or legacy findings outside changed lines under + **`--enforcement changed`** +- `1`: `FAIL` under **`--enforcement full`**, or a blocking finding on a + changed line under **`--enforcement changed`** - `2`: invalid CLI usage, such as a missing file path or incompatible options ### Output modes @@ -202,7 +204,7 @@ specfact code review run --fix packages/specfact-code-review/src/specfact_code_r For simplify-focused cleanup, prefer a JSON-first preview loop before writing: ```bash -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json ``` Inspect `cleanup_forecast` to estimate cleanup yield and sort by diff --git a/docs/quickstart-ai-bloat.md b/docs/quickstart-ai-bloat.md index 0880a301..5cb8c2c7 100644 --- a/docs/quickstart-ai-bloat.md +++ b/docs/quickstart-ai-bloat.md @@ -23,10 +23,10 @@ specfact init ide ## 2. Run simplify review with cleanup forecast evidence ```bash -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json ``` -Omit `--level` for this report. `--level error` intentionally filters info-level findings, including `ai_bloat`, out of the command output. `--preview-fixes` is non-mutating: it adds patch forecast evidence without editing tracked files. +Omit `--level` for this report. `--level error` intentionally filters info-level findings, including `ai_bloat`, out of the command output. Use `--enforcement shadow` for the evidence-gathering pass so legacy blockers do not prevent the JSON handoff. `--preview-fixes` is non-mutating: it adds patch forecast evidence without editing tracked files. ## 3. Inspect the signal @@ -88,7 +88,7 @@ def double(values: list[int]) -> list[int]: ## 5. Re-run review ```bash -specfact code review run --scope changed --focus simplify --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --json --out .specfact/code-review-simplify.json ``` Use the new report to confirm accepted simplifications cleared the corresponding `ai_bloat` findings. This is bloat-shape detection, not AI-authorship detection. diff --git a/docs/reference/README.md b/docs/reference/README.md index 887f8236..6c4910bf 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -34,12 +34,12 @@ Complete technical reference for the official modules site and bundle-owned work ### Commands -- `specfact sync bridge --adapter speckit --bundle ` - Import from external tools via bridge adapter +- `specfact project sync bridge --adapter speckit --bundle ` - Import from external tools via bridge adapter - `specfact code import ` - Reverse-engineer plans from code - `specfact code analyze contracts` - Analyze contract coverage for a codebase bundle - `specfact govern enforce stage` - Configure quality gates - `specfact code repro` - Run the reproducibility validation suite -- `specfact sync bridge --adapter --bundle ` - Sync with external tools via bridge adapter +- `specfact project sync bridge --adapter --bundle ` - Sync with external tools via bridge adapter - `specfact spec validate [--bundle ]` - Validate OpenAPI/AsyncAPI specifications - `specfact spec generate-tests [--bundle ]` - Generate contract tests from specifications - `specfact spec mock [--bundle ]` - Launch mock server for development diff --git a/docs/reference/command-syntax-policy.md b/docs/reference/command-syntax-policy.md index a61eda90..c9883765 100644 --- a/docs/reference/command-syntax-policy.md +++ b/docs/reference/command-syntax-policy.md @@ -24,7 +24,7 @@ Always document commands exactly as implemented by the relevant current help ent - Positional bundle argument: - `specfact code import [BUNDLE]` - `--bundle` option: - - Supported by commands such as `specfact sync bridge --bundle ` + - Supported by commands such as `specfact project sync bridge --bundle ` - Not universally supported across all commands, so always verify with `--help` For callback-style commands such as `specfact code import`, keep options before the positional bundle argument in examples, for example `specfact code import --repo . legacy-api`. @@ -47,7 +47,7 @@ Before merging command docs updates: ```bash hatch run specfact code import --help -hatch run specfact sync bridge --help +hatch run specfact project sync bridge --help hatch run specfact code validate sidecar --help hatch run specfact govern enforce --help ``` diff --git a/docs/reference/commands.generated.json b/docs/reference/commands.generated.json new file mode 100644 index 00000000..c9e438de --- /dev/null +++ b/docs/reference/commands.generated.json @@ -0,0 +1,1793 @@ +[ + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact backlog", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [ + "add", + "analyze-deps", + "auth", + "ceremony", + "daily", + "delta", + "diff", + "init-config", + "map-fields", + "promote", + "refine", + "sync", + "verify-readiness" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog add", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--acceptance-criteria", + "--adapter", + "--body", + "--body-end-marker", + "--business-value", + "--check-dor", + "--custom-config", + "--description-format", + "--non-interactive", + "--parent", + "--priority", + "--project-id", + "--provider-field", + "--repo-path", + "--sprint", + "--story-points", + "--template", + "--title", + "--type" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog analyze-deps", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--custom-config", + "--json-export", + "--output", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact backlog auth", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [ + "azure-devops", + "clear", + "github", + "status" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog auth azure-devops", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--pat", + "--use-device-code" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog auth clear", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--provider" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog auth github", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--base-url", + "--client-id", + "--scopes" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog auth status", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact backlog ceremony", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [ + "flow", + "pi-summary", + "planning", + "refinement", + "standup" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog ceremony flow", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--mode" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog ceremony pi-summary", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--mode" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog ceremony planning", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--mode" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog ceremony refinement", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog ceremony standup", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--mode" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog daily", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact backlog delta", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [ + "cost-estimate", + "impact", + "rollback-analysis", + "status" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog delta cost-estimate", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--baseline-file", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog delta impact", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog delta rollback-analysis", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--baseline-file", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog delta status", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--baseline-file", + "--project-id", + "--repo-name", + "--repo-owner", + "--since", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog diff", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--baseline-file", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog init-config", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--force" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog map-fields", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog promote", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--item-id", + "--project-id", + "--template", + "--to-status" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog refine", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog sync", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--baseline-file", + "--force-baseline-overwrite", + "--output-format", + "--project-id", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact backlog verify-readiness", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-backlog", + "options": [ + "--adapter", + "--project-id", + "--target-items", + "--template" + ], + "owner_package": "nold-ai/specfact-backlog", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_backlog.backlog.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "analyze", + "drift", + "import", + "repro", + "validate" + ] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code analyze", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "contracts" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code analyze contracts", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--bundle", + "--repo" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code drift", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "detect" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code drift detect", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--format", + "--out", + "--repo" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code import", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "from-bridge", + "from-code" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code import from-bridge", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--adapter", + "--dry-run", + "--force", + "--out-branch", + "--repo", + "--report", + "--write" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code import from-code", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code repro", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--budget", + "--crosshair-per-path-timeout", + "--crosshair-required", + "--fail-fast", + "--fix", + "--out", + "--repo", + "--sidecar", + "--sidecar-bundle", + "--verbose" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "setup" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code repro setup", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--install-crosshair", + "--repo" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code review", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [ + "review" + ] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code review review", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [ + "ledger", + "rules", + "run" + ] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code review review ledger", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [ + "reset", + "status", + "update" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review ledger reset", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--confirm" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review ledger status", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review ledger update", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--from" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code review review rules", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [ + "init", + "show", + "update" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review rules init", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--ide" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review rules show", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review rules update", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--ide" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review review run", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code validate", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "sidecar" + ] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code validate sidecar", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [ + "init", + "run" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code validate sidecar init", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code validate sidecar run", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-codebase", + "options": [ + "--no-run-crosshair", + "--no-run-specmatic", + "--run-crosshair", + "--run-specmatic" + ], + "owner_package": "nold-ai/specfact-codebase", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_codebase.code.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact govern", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [ + "enforce", + "patch" + ] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact govern enforce", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [ + "sdd", + "stage" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact govern enforce sdd", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [ + "--no-interactive", + "--out", + "--output-format", + "--sdd" + ], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact govern enforce stage", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [ + "--preset" + ], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact govern patch", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [ + "apply" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact govern patch apply", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-govern", + "options": [ + "--dry-run", + "--write", + "--yes" + ], + "owner_package": "nold-ai/specfact-govern", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_govern.govern.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact project", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [ + "devops-flow", + "export", + "export-roadmap", + "health-check", + "import", + "init-personas", + "link-backlog", + "lock", + "locks", + "merge", + "regenerate", + "resolve-conflict", + "snapshot", + "sync", + "unlock", + "version" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project devops-flow", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--action", + "--bundle", + "--no-interactive", + "--project-name", + "--repo", + "--stage", + "--verbose" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project export", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--list-personas", + "--no-interactive", + "--out", + "--output", + "--output-dir", + "--persona", + "--repo", + "--stdout", + "--template" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project export-roadmap", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--output", + "--project-name", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project health-check", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--project-name", + "--repo", + "--verbose" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project import", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--dry-run", + "--file", + "--input", + "--no-interactive", + "--persona", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project init-personas", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--persona", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project link-backlog", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--adapter", + "--bundle", + "--no-interactive", + "--project-id", + "--project-name", + "--repo", + "--template" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project lock", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--persona", + "--repo", + "--section" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project locks", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project merge", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--base", + "--bundle", + "--no-interactive", + "--ours", + "--out", + "--output", + "--persona-ours", + "--persona-theirs", + "--repo", + "--strategy", + "--theirs" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project regenerate", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--project-name", + "--repo", + "--strict", + "--verbose" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project resolve-conflict", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--path", + "--persona", + "--repo", + "--resolution" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project snapshot", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--output", + "--project-name", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact project sync", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [ + "bridge", + "intelligent", + "repository" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project sync bridge", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--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" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project sync intelligent", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--code-to-spec", + "--repo", + "--spec-to-code", + "--tests", + "--watch" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project sync repository", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--confidence", + "--interval", + "--repo", + "--target", + "--watch" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project unlock", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--no-interactive", + "--repo", + "--section" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact project version", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [ + "bump", + "check", + "set" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project version bump", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--repo", + "--type" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project version check", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--repo" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact project version set", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-project", + "options": [ + "--bundle", + "--repo", + "--version" + ], + "owner_package": "nold-ai/specfact-project", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_project.project.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact spec", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-spec", + "options": [ + "--install-completion", + "--show-completion" + ], + "owner_package": "nold-ai/specfact-spec", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_spec.spec.commands:app", + "subcommands": [ + "backward-compat", + "generate-tests", + "mock", + "validate" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact spec backward-compat", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-spec", + "options": [], + "owner_package": "nold-ai/specfact-spec", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_spec.spec.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact spec generate-tests", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-spec", + "options": [ + "--bundle", + "--force", + "--out", + "--output" + ], + "owner_package": "nold-ai/specfact-spec", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_spec.spec.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact spec mock", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-spec", + "options": [ + "--bundle", + "--examples", + "--no-interactive", + "--port", + "--spec", + "--strict" + ], + "owner_package": "nold-ai/specfact-spec", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_spec.spec.commands:app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact spec validate", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-spec", + "options": [ + "--bundle", + "--force", + "--no-interactive", + "--previous" + ], + "owner_package": "nold-ai/specfact-spec", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_spec.spec.commands:app", + "subcommands": [] + } +] diff --git a/docs/reference/commands.generated.md b/docs/reference/commands.generated.md new file mode 100644 index 00000000..a3991d43 --- /dev/null +++ b/docs/reference/commands.generated.md @@ -0,0 +1,100 @@ +--- +layout: default +title: Generated SpecFact Module Command Overview +permalink: /reference/generated-module-command-overview/ +exempt: true +exempt_reason: Generated command contract artifact. +--- + +# Generated SpecFact Module Command Overview + +This file is generated from the current module command trees. Do not edit by hand. + +| Command | Module | Install | Options | Subcommands | Context | +| --- | --- | --- | --- | --- | --- | +| `specfact backlog` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --install-completion, --show-completion; args: - | add, analyze-deps, auth, ceremony, daily, delta, diff, init-config, map-fields, promote, refine, sync, verify-readiness | | +| `specfact backlog add` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --acceptance-criteria, --adapter, --body, --body-end-marker, --business-value, --check-dor, --custom-config, --description-format, --non-interactive, --parent, --priority, --project-id, --provider-field, --repo-path, --sprint, --story-points, --template, --title, --type; args: - | - | | +| `specfact backlog analyze-deps` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --custom-config, --json-export, --output, --project-id, --template; args: - | - | | +| `specfact backlog auth` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | azure-devops, clear, github, status | | +| `specfact backlog auth azure-devops` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --pat, --use-device-code; args: - | - | | +| `specfact backlog auth clear` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --provider; args: - | - | | +| `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 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 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 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 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 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 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 | | +| `specfact code review review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | ledger, rules, run | | +| `specfact code review review ledger` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | reset, status, update | | +| `specfact code review review ledger reset` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --confirm; args: - | - | | +| `specfact code review review ledger status` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | +| `specfact code review review ledger update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --from; args: - | - | | +| `specfact code review review rules` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | init, show, update | | +| `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 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 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 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 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: - | - | | +| `specfact project export-roadmap` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --output, --project-name, --repo; args: - | - | | +| `specfact project health-check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --project-name, --repo, --verbose; args: - | - | | +| `specfact project import` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --dry-run, --file, --input, --no-interactive, --persona, --repo; args: - | - | | +| `specfact project init-personas` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --persona, --repo; args: - | - | | +| `specfact project link-backlog` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --adapter, --bundle, --no-interactive, --project-id, --project-name, --repo, --template; args: - | - | | +| `specfact project lock` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --persona, --repo, --section; args: - | - | | +| `specfact project locks` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --repo; args: - | - | | +| `specfact project merge` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --base, --bundle, --no-interactive, --ours, --out, --output, --persona-ours, --persona-theirs, --repo, --strategy, --theirs; args: - | - | | +| `specfact project regenerate` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --project-name, --repo, --strict, --verbose; args: - | - | | +| `specfact project resolve-conflict` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --path, --persona, --repo, --resolution; args: - | - | | +| `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 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 | | +| `specfact project version bump` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --type; args: - | - | | +| `specfact project version check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo; args: - | - | | +| `specfact project version set` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --version; args: - | - | | +| `specfact 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 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: - | - | | diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 8ab86fe9..8bdd1dcf 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -59,7 +59,7 @@ specfact module install nold-ai/specfact-backlog # Project workflow examples specfact code import --repo . legacy-api -specfact sync bridge --adapter github --mode export-only --repo . +specfact project sync bridge --adapter github --mode export-only --repo . # Code workflow examples specfact code validate sidecar init legacy-api /path/to/repo diff --git a/docs/team-and-enterprise/multi-repo.md b/docs/team-and-enterprise/multi-repo.md index bbc854a9..852a54cf 100644 --- a/docs/team-and-enterprise/multi-repo.md +++ b/docs/team-and-enterprise/multi-repo.md @@ -30,7 +30,7 @@ Commands that support `--repo` should point to the active repository when automa ```bash specfact project export --repo /workspace/service-a --bundle service-a --persona architect --stdout specfact project import --repo /workspace/service-b --bundle service-b --persona developer --input docs/project-plans/developer.md --dry-run -specfact sync bridge --adapter github --mode export-only --repo /workspace/service-a --bundle service-a +specfact project sync bridge --adapter github --mode export-only --repo /workspace/service-a --bundle service-a ``` ## 3. Keep shared module rollout predictable diff --git a/llms.txt b/llms.txt new file mode 100644 index 00000000..9204718e --- /dev/null +++ b/llms.txt @@ -0,0 +1,104 @@ +# SpecFact Module Commands + +Use this generated overview as the current module command contract before following older docs or prompts. + +--- +layout: default +title: Generated SpecFact Module Command Overview +permalink: /reference/generated-module-command-overview/ +exempt: true +exempt_reason: Generated command contract artifact. +--- + +# Generated SpecFact Module Command Overview + +This file is generated from the current module command trees. Do not edit by hand. + +| Command | Module | Install | Options | Subcommands | Context | +| --- | --- | --- | --- | --- | --- | +| `specfact backlog` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --install-completion, --show-completion; args: - | add, analyze-deps, auth, ceremony, daily, delta, diff, init-config, map-fields, promote, refine, sync, verify-readiness | | +| `specfact backlog add` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --acceptance-criteria, --adapter, --body, --body-end-marker, --business-value, --check-dor, --custom-config, --description-format, --non-interactive, --parent, --priority, --project-id, --provider-field, --repo-path, --sprint, --story-points, --template, --title, --type; args: - | - | | +| `specfact backlog analyze-deps` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --adapter, --custom-config, --json-export, --output, --project-id, --template; args: - | - | | +| `specfact backlog auth` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | -; args: - | azure-devops, clear, github, status | | +| `specfact backlog auth azure-devops` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --pat, --use-device-code; args: - | - | | +| `specfact backlog auth clear` | nold-ai/specfact-backlog | `specfact module install nold-ai/specfact-backlog` | --provider; args: - | - | | +| `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 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 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 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 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 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 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 | | +| `specfact code review review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | ledger, rules, run | | +| `specfact code review review ledger` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | reset, status, update | | +| `specfact code review review ledger reset` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --confirm; args: - | - | | +| `specfact code review review ledger status` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | +| `specfact code review review ledger update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --from; args: - | - | | +| `specfact code review review rules` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | init, show, update | | +| `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 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 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 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 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: - | - | | +| `specfact project export-roadmap` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --output, --project-name, --repo; args: - | - | | +| `specfact project health-check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --project-name, --repo, --verbose; args: - | - | | +| `specfact project import` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --dry-run, --file, --input, --no-interactive, --persona, --repo; args: - | - | | +| `specfact project init-personas` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --persona, --repo; args: - | - | | +| `specfact project link-backlog` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --adapter, --bundle, --no-interactive, --project-id, --project-name, --repo, --template; args: - | - | | +| `specfact project lock` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --persona, --repo, --section; args: - | - | | +| `specfact project locks` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --repo; args: - | - | | +| `specfact project merge` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --base, --bundle, --no-interactive, --ours, --out, --output, --persona-ours, --persona-theirs, --repo, --strategy, --theirs; args: - | - | | +| `specfact project regenerate` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --project-name, --repo, --strict, --verbose; args: - | - | | +| `specfact project resolve-conflict` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --no-interactive, --path, --persona, --repo, --resolution; args: - | - | | +| `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 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 | | +| `specfact project version bump` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --type; args: - | - | | +| `specfact project version check` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo; args: - | - | | +| `specfact project version set` | nold-ai/specfact-project | `specfact module install nold-ai/specfact-project` | --bundle, --repo, --version; args: - | - | | +| `specfact 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 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: - | - | | diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index 14e1b0c0..2db1d81f 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -41,6 +41,7 @@ | Module | Order | Change folder | GitHub # | Blocked by | |--------|-------|---------------|----------|------------| +| backlog-core | 00 | tester-module-cli-reliability | [#306](https://github.com/nold-ai/specfact-cli-modules/issues/306); source bugs [specfact-cli#586](https://github.com/nold-ai/specfact-cli/issues/586), [#587](https://github.com/nold-ai/specfact-cli/issues/587), [#588](https://github.com/nold-ai/specfact-cli/issues/588), [#590](https://github.com/nold-ai/specfact-cli/issues/590), [#591](https://github.com/nold-ai/specfact-cli/issues/591), [#592](https://github.com/nold-ai/specfact-cli/issues/592) | paired core `tester-cli-reliability`; Parent Feature: [#305](https://github.com/nold-ai/specfact-cli-modules/issues/305) | | backlog-scrum | 02 | backlog-scrum-02-sprint-planning | [#160](https://github.com/nold-ai/specfact-cli-modules/issues/160) | Parent Feature: [#151](https://github.com/nold-ai/specfact-cli-modules/issues/151); shared backlog baseline from `specfact-cli#116` | | backlog-scrum | 03 | backlog-scrum-03-story-complexity | [#153](https://github.com/nold-ai/specfact-cli-modules/issues/153) | Parent Feature: [#151](https://github.com/nold-ai/specfact-cli-modules/issues/151); shared backlog baseline from `specfact-cli#116` | | backlog-scrum | 04 | backlog-scrum-04-definition-of-done | [#152](https://github.com/nold-ai/specfact-cli-modules/issues/152) | Parent Feature: [#151](https://github.com/nold-ai/specfact-cli-modules/issues/151); shared backlog baseline from `specfact-cli#116`; optional ceremony alias baseline `specfact-cli#185` | diff --git a/openspec/changes/tester-module-cli-reliability/.openspec.yaml b/openspec/changes/tester-module-cli-reliability/.openspec.yaml new file mode 100644 index 00000000..927e3e8e --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-31 diff --git a/openspec/changes/tester-module-cli-reliability/TDD_EVIDENCE.md b/openspec/changes/tester-module-cli-reliability/TDD_EVIDENCE.md new file mode 100644 index 00000000..9632fb1a --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/TDD_EVIDENCE.md @@ -0,0 +1,223 @@ +# TDD Evidence: tester-module-cli-reliability + +## Readiness + +- Worktree: `/home/dom/git/nold-ai/specfact-cli-modules-worktrees/feature/tester-command-reliability` +- Branch: `feature/tester-command-reliability` +- Modules feature: nold-ai/specfact-cli-modules#305 +- Modules story: nold-ai/specfact-cli-modules#306 +- Paired core story: nold-ai/specfact-cli#594 + +## Source Ownership + +- `nold-ai/specfact-cli#586`: module-owned `specfact project regenerate` runtime hardening. +- `nold-ai/specfact-cli#587`: split; modules owns canonical `specfact project sync bridge` help/docs/prompts. +- `nold-ai/specfact-cli#588`: split; modules owns `specfact code import` command contract/help. +- `nold-ai/specfact-cli#590`: split; modules owns codebase/code-review semgrep diagnostic adoption. +- `nold-ai/specfact-cli#591`: module-owned backlog auth missing-subcommand UX. +- `nold-ai/specfact-cli#592`: module-owned backlog delta status config/default contract. + +## Failing Before + +- Targeted module regression suite run from the paired core Hatch environment with module packages on `PYTHONPATH` -> 6 failed before production edits. + - `backlog auth` without a subcommand only emitted Click's generic missing-command error and did not print available auth subcommands. + - `backlog delta status github` required `--project-id` and did not resolve documented provider config or repo owner/name flags. + - `project sync bridge --help` did not expose the canonical `specfact project sync bridge` path expected by docs/tests. + - `code import service-a --repo .` did not provide migration guidance to supported canonical ordering. + - Prompt command validation still modeled removed flat shims such as `specfact sync`, `specfact plan`, `specfact import`, and `specfact migrate`. +- `PYTHONPATH=: hatch run pytest tests/unit/test_global_cli_error_contract.py -q` after adding global module-context tests -> 2 failed before the shared core renderer was imported for direct module apps. + - Direct module groups did not consistently emit help plus missing-subcommand guidance. + - Direct module command contexts did not inherit the shared missing-parameter renderer. + +## OpenSpec Validation + +- `openspec validate tester-module-cli-reliability --strict` -> passed. + +## Passing After + +- `PYTHONPATH=: hatch run python scripts/generate-command-overview.py --check` -> passed. +- `hatch run check-command-contract` -> passed: `check-command-contract: OK (86 generated module command path(s) validated)`. +- `PYTHONPATH=: hatch run python scripts/check-docs-commands.py` -> passed: `Docs command validation passed with no findings.` +- `PYTHONPATH=: hatch run python scripts/check-prompt-commands.py` -> passed: `Prompt command validation passed with no findings.` +- `PYTHONPATH=: hatch run pytest tests/unit/test_global_cli_error_contract.py tests/unit/specfact_backlog/test_auth_commands.py tests/unit/specfact_backlog/test_delta_command_contract.py tests/e2e/specfact_project/test_help_smoke.py::test_project_sync_bridge_help_uses_canonical_command_path tests/unit/specfact_codebase/test_import_command_contract.py tests/unit/test_check_prompt_commands_script.py::test_module_app_mounts_do_not_include_removed_flat_shims -q` -> 11 passed. +- Paired core Hatch environment with modules packages on `PYTHONPATH`: `hatch run pytest /home/dom/git/nold-ai/specfact-cli-modules-worktrees/feature/tester-command-reliability/tests/unit/test_global_cli_error_contract.py -q` -> 2 passed. +- `hatch run pytest tests/unit/specfact_codebase/test_import_command_contract.py tests/unit/test_global_cli_error_contract.py -q` -> 3 passed. +- `hatch run pytest tests/unit/specfact_project/test_regenerate_command_contract.py -q` -> 1 failed before the `project regenerate` null-graph guard because the command still raised a raw `NoneType` attribute error. +- `hatch run pytest tests/e2e/specfact_project/test_help_smoke.py::test_project_sync_bridge_help_uses_canonical_command_path tests/unit/specfact_project/test_regenerate_command_contract.py -q` -> 2 passed after the typed `project regenerate` diagnostic and canonical sync-bridge help checks. +- `hatch run pytest tests/unit/test_check_prompt_commands_script.py::test_iter_prompt_paths_includes_resource_templates tests/unit/test_check_prompt_commands_script.py::test_validate_prompt_commands_reports_stale_command_in_resource_template tests/unit/test_check_prompt_commands_script.py::test_docs_review_workflow_runs_prompt_command_validation tests/unit/test_check_prompt_commands_script.py::test_pre_commit_prompt_validation_covers_cli_command_implementations -q` -> 4 passed after extending prompt command validation to module resource templates, YAML/Jinja2/text/JSON assets, pre-commit, and docs-review path filters. +- Release hygiene: + - Changed module package manifests were patch-bumped: `specfact-backlog` `0.41.26`, `specfact-codebase` `0.41.11`, `specfact-govern` `0.40.22`, `specfact-project` `0.41.19`, and `specfact-spec` `0.40.19`. + - `hatch run python scripts/sign-modules.py --allow-unsigned --payload-from-filesystem packages/specfact-backlog/module-package.yaml packages/specfact-codebase/module-package.yaml packages/specfact-govern/module-package.yaml packages/specfact-project/module-package.yaml packages/specfact-spec/module-package.yaml` -> passed, refreshing payload checksums. No signing key variables were configured in this shell, so this was checksum-only local signing. + - `hatch run python scripts/verify-modules-signature.py --payload-from-filesystem --enforce-version-bump --version-check-base origin/dev` -> passed. + - `hatch run generate-command-overview` -> passed after the manifest version bumps. + - `hatch run check-command-overview` -> passed after regeneration. + - `hatch run python scripts/check-docs-commands.py` -> passed after regeneration. +- `specfact code import from-code --help` and `specfact code import from-bridge --help` now render explicit subcommand usage instead of parent `code import` usage; `from-code` is visible in the generated command overview and `llms.txt`. +- `scripts/pre-commit-quality-checks.sh` regenerates and stages module `llms.txt`, `docs/reference/commands.generated.json`, and `docs/reference/commands.generated.md`, then validates overview freshness and source-backed command behavior before docs/prompt command validation. +- PR validation now checks module command overview freshness, generated command contract behavior, docs/prompt command references, and delegates the package-manager runtime smoke to the paired core workflow checkout. +- `openspec validate tester-module-cli-reliability --strict` -> passed after the focused global contract rerun. +- CI duplicate full-suite hardening: + - Modules PR orchestrator now has one full-suite owner: `hatch run test`. + - Contract validation now runs `hatch run contract-test-contracts`; smart-test validation now runs configuration-only `hatch run smart-test-check`. + - The broad `contract-test` alias now maps to scoped contract checks, not the full smart-test runner. + - Pre-commit fallback and the PR template now point to `hatch run contract-test-contracts`, `hatch run smart-test-check`, and `hatch run test` instead of encouraging three broad test invocations. + - `hatch run pytest tests/unit/workflows/test_pr_orchestrator_signing.py tests/unit/tools/test_contract_first_smart_test.py -q` -> 10 passed. + - `hatch run pytest tests/unit/workflows/test_pr_orchestrator_signing.py -q` -> 6 passed after the PR template/pre-commit wording updates. + - `hatch run contract-test -q` -> 28 passed, 785 deselected, 2 warnings, confirming the legacy alias is now scoped contract validation. +- Quality gates after CI duplicate hardening: + - `hatch run format` -> passed. + - `hatch run type-check` -> passed. + - `hatch run lint` -> passed. + - `hatch run yaml-lint` -> passed. + - `openspec validate tester-module-cli-reliability --strict` -> passed. +- SpecFact code review bug-hunt: + - Initial `specfact code review run --scope changed --bug-hunt --include-tests --json --out .specfact/code-review.changed.json` found actionable slice blockers in `backlog_core/commands/delta.py`, `specfact_codebase/repro/commands.py`, and `scripts/check-command-contract.py`. + - Fixed the actionable blockers by adding an explicit typed guard after missing delta context exits, replacing unused TOML fallback imports with `importlib.util.find_spec`, and casting the generated Typer app before runner invocation. + - Rerun with paired module source wired through `SPECFACT_MODULES_ROOTS`/`PYTHONPATH` -> `Review completed with 396 findings (161 blocking)`. + - Remaining blockers are legacy changed-file-scope findings in large pre-existing module command implementations: 56 `clean_code` complexity findings, 92 `kiss` size/nesting/parameter findings, 12 private unused-function findings, and 1 pylint timeout `tool_error`. They are not introduced by the command reliability edits, but the review tool reports them because those files contain updated command help text and are therefore in changed scope. + +## Deferred / Not Covered In This Slice + +- Full `smart-test` was not rerun after narrowing the PR workflow, because the targeted workflow regression suite and scoped `contract-test` now verify the duplicate full-suite behavior directly. +- Refactoring the 161 remaining legacy modules code-review blockers requires a separate broad cleanup change across `specfact-project`, `specfact-codebase`, `specfact-govern`, and `specfact-spec`; doing that inside the tester command reliability patch would change unrelated command internals with high regression risk. + +## Follow-up Review Fixes + +- Addressed follow-up CI/review findings: + - Docs review and PR orchestrator workflows now resolve a matching paired `specfact-cli` branch when present, falling back to the PR base branch (`main` or `dev`) and then `dev`. + - Touched checkout steps set `persist-credentials: false`. + - Runtime discovery smoke in modules CI now runs via `hatch run python specfact-cli/scripts/runtime_discovery_smoke.py` so the paired core script can import its dependencies. + - Generated command overview no longer marks callback-only help/error groups such as `specfact backlog auth` as executable, while preserving executable callback groups such as `specfact code import` and `specfact code repro`. + - Prompt command validation now indexes nested Typer groups and Typer option metadata by attribute, matching the generated command overview behavior. + - Pre-commit refuses to auto-stage generated command artifacts when command overview inputs have unstaged changes. + - `tasks.md` quality/review checklist now matches the recorded evidence and documented review exception. +- Follow-up verification: + - `hatch run pytest tests/unit/test_check_prompt_commands_script.py tests/unit/workflows/test_pr_orchestrator_signing.py tests/unit/test_check_docs_commands_script.py tests/unit/test_pre_commit_quality_parity.py -q` -> 39 passed. + - `hatch run yaml-lint && hatch run check-command-overview && hatch run check-command-contract && hatch run python scripts/check-docs-commands.py && hatch run python scripts/check-prompt-commands.py && openspec validate tester-module-cli-reliability --strict` -> passed. + - `hatch run python /home/dom/git/nold-ai/specfact-cli-worktrees/feature/tester-command-reliability/scripts/runtime_discovery_smoke.py --modules-repo /home/dom/git/nold-ai/specfact-cli-modules-worktrees/feature/tester-command-reliability --launcher pipx --launcher uv-run --launcher uvx` -> passed for all three remaining package-manager launchers. + - `hatch run format` -> passed. + - `hatch run lint` -> failed on an existing unrelated type mismatch in `tests/unit/specfact_backlog/conftest.py` (`typer.testing.Result` vs `click.testing.Result`); none of the follow-up edits touched that file. Focused touched-scope tests and validators above pass. + +## Follow-up PR Thread Fixes + +- Validated live PR #307 review threads and CI annotations after the previous follow-up. +- Addressed remaining actionable findings: + - Paired core checkout steps in touched workflows now pin `actions/checkout` to `34e114876b0b11c390a56381ad16ebd13914f8d5` while retaining `persist-credentials: false`. + - `backlog delta status` falls back to missing-context guidance when `.specfact/backlog-config.yaml` is malformed YAML instead of leaking parser errors. + - `project snapshot` now uses the same typed backlog-graph guard as `project regenerate`. + - Semgrep plugin status preserves the active environment probe message returned by core `check_tool_in_env`. + - Generated command JSON loading in `scripts/check-docs-commands.py` fails fast on malformed JSON or malformed entries. + - Project, govern, and spec prompt guidance no longer uses `specfact project --help` as an executable workflow placeholder; examples now use concrete generated-contract commands such as `code import from-code`, `project health-check`, `project export`, and `govern enforce sdd`. + - Project overview docs were reduced to command families present in the generated project command contract. + - OpenSpec source tracking now includes source bug `#589`. +- Follow-up verification: + - `hatch run pytest tests/unit/specfact_backlog/test_delta_command_contract.py tests/unit/specfact_project/test_regenerate_command_contract.py tests/unit/specfact_project/test_code_analyzer_semgrep_status.py tests/unit/test_check_docs_commands_script.py tests/unit/test_check_prompt_commands_script.py tests/unit/workflows/test_pr_orchestrator_signing.py tests/unit/test_pre_commit_quality_parity.py -q` -> 47 passed. + - `hatch run yaml-lint && hatch run check-command-overview && hatch run check-command-contract && hatch run python scripts/check-docs-commands.py && hatch run python scripts/check-prompt-commands.py && openspec validate tester-module-cli-reliability --strict` -> passed. + - `hatch run format` -> passed. + +## Follow-up Code Review Enforcement Modes + +- Added explicit code-review enforcement policies: + - `full`: strict mode; any blocking finding in reviewed files blocks the run. + - `changed`: default CLI/pre-commit mode; blocking findings only block when they target changed lines, while legacy blockers remain in JSON evidence. + - `shadow`: evidence-only mode; findings are reported but the run does not block. +- Runtime and gate wiring: + - `specfact code review run --enforcement full|changed|shadow` is now the primary runtime option. + - Deprecated `--mode enforce|shadow` remains supported as a compatibility alias (`enforce` maps to `full`). + - Pre-commit/CI wrapper reads `SPECFACT_CODE_REVIEW_ENFORCEMENT`, defaults to `changed`, and uses cached staged diffs for changed-line evidence. + - Checked the shipped GitHub workflow Jinja template; it does not invoke code review, so no PR review template change was required. +- Follow-up verification: + - `hatch run pytest tests/unit/scripts/test_pre_commit_code_review.py tests/unit/specfact_code_review/run/test_runner.py tests/unit/specfact_code_review/run/test_commands.py tests/unit/specfact_code_review/review/test_commands.py tests/unit/docs/test_code_review_docs_parity.py tests/unit/test_pre_commit_quality_parity.py -q` -> included in the final combined 172-test focused suite. + - `hatch run pytest tests/unit/specfact_backlog/test_delta_command_contract.py tests/unit/specfact_project/test_regenerate_command_contract.py tests/unit/specfact_project/test_code_analyzer_semgrep_status.py tests/unit/test_check_docs_commands_script.py tests/unit/test_check_prompt_commands_script.py tests/unit/workflows/test_pr_orchestrator_signing.py tests/unit/test_pre_commit_quality_parity.py tests/unit/scripts/test_pre_commit_code_review.py tests/unit/specfact_code_review/run/test_runner.py tests/unit/specfact_code_review/run/test_commands.py tests/unit/specfact_code_review/review/test_commands.py tests/unit/docs/test_code_review_docs_parity.py -q` -> 172 passed. + - `hatch run generate-command-overview` -> passed. + - `hatch run check-command-overview` -> passed. + - `hatch run check-command-contract` -> passed: `check-command-contract: OK (86 generated module command path(s) validated)`. + - `hatch run python scripts/check-docs-commands.py` -> passed. + - `hatch run python scripts/check-prompt-commands.py` -> passed. + - `hatch run lint` -> passed. + - Documentation follow-up after user review: + - Updated broader user-facing docs, tutorials, guides, bundled skills, and `/specfact.08-simplify` prompt examples to show `--enforcement full|changed|shadow`. + - `hatch run python scripts/check-docs-commands.py` -> passed. + - `hatch run python scripts/check-prompt-commands.py` -> passed. + - `hatch run pytest tests/unit/docs/test_code_review_docs_parity.py tests/unit/test_guided_simplify_resources.py tests/unit/specfact_code_review/rules/test_updater.py tests/unit/test_check_prompt_commands_script.py -q` -> 41 passed. + +## Second Follow-up PR CI Fixes + +- Re-checked paired core PR #595 after pushing modules. Fresh core CLI validation failed while importing paired modules: `specfact_backlog/backlog_core/commands/delta.py` imported `typer._click.core`, which is not available under the core CI Typer dependency range. +- Fixed the backlog delta status callback to use public `typer.Context` instead of Typer's private vendored Click namespace. +- Added `test_delta_command_avoids_private_typer_click_import`. +- Follow-up verification: + - `hatch run pytest tests/unit/specfact_backlog/test_delta_command_contract.py -q` -> 4 passed. + - `rg -n "from typer\\._click|TyperClickContext" packages scripts docs` -> no matches. + - `hatch run check-command-overview` -> passed. + - `hatch run check-command-contract` -> passed: `check-command-contract: OK (86 generated module command path(s) validated)`. + - `hatch run lint` -> passed. + - `openspec validate tester-module-cli-reliability --strict` -> passed. + +## Third Follow-up PR Review Fixes + +- Re-checked live PR #307 review threads after the enforcement-mode documentation updates. +- Addressed still-valid findings: + - Changed enforcement now skips unreadable or non-UTF-8 untracked files instead of crashing when collecting changed-line evidence. + - Prompt command validation and command overview generation no longer assume every Click parameter with `opts` also exposes `secondary_opts`. + - Typer app conversion sites in prompt validation and command overview generation now cast the generated Click command explicitly, resolving follow-up type-safety review errors. + - Pre-commit changed-line parsing only treats `+++ ` as a destination-file header when it follows a `--- ` source header, so staged content lines beginning with `++ ` cannot corrupt the changed-line map. + - `/specfact.04-sdd` and `/specfact.07-contracts` prompt parameter docs now describe active-plan fallback consistently. + - `/specfact.04-sdd` no longer claims a distinct plan-update CLI command exists before SDD regeneration. +- Reviewed and intentionally kept the `changed` default for `specfact code review run --enforcement`: this is the requested default policy for legacy-noise-tolerant gates, while `full` remains available and documented for strict CI/pre-commit enforcement. +- Reviewed the remaining advisory code-review warnings after the final amend. Fixed the valid line-length warning. Left the generator script's `print()` and missing-contract warnings as documented exceptions: this existing CLI utility intentionally writes generated diffs/status to stdout, and adding icontract decorators to its existing script entry points is outside the review-thread fix scope. +- Follow-up verification: + - `hatch run format` -> passed. + - `hatch run pytest tests/unit/test_pre_commit_quality_parity.py tests/unit/specfact_code_review/run/test_runner.py tests/unit/test_check_prompt_commands_script.py -q` -> 66 passed. + - `hatch run pytest tests/unit/test_check_prompt_commands_script.py tests/unit/test_pre_commit_quality_parity.py -q` -> 26 passed after the final type-safety cleanup. + - `hatch run python scripts/check-prompt-commands.py` -> passed. + - `hatch run generate-command-overview` -> passed. + - `hatch run check-command-overview` -> passed. + - `hatch run check-command-contract` -> passed: `check-command-contract: OK (86 generated module command path(s) validated)`. + - `hatch run python scripts/check-docs-commands.py` -> passed. + - `hatch run lint` -> passed. + - `openspec validate tester-module-cli-reliability --strict` -> passed. + +## Fourth Follow-up PR Review Fixes + +- Re-checked the seven PR #307 CodeRabbit findings against current code: + - Kept `changed` as the default enforcement mode because it is the requested default policy, and added an explicit runtime notice that `--enforcement full` is required for strict CI gates. + - Confirmed unreadable untracked files are skipped safely when collecting changed-line evidence. + - Confirmed `/specfact.04-sdd` and `/specfact.07-contracts` active-plan/default prompt text is corrected. + - Confirmed `_command_options` guards `secondary_opts`. + - Confirmed staged diff parsing only recognizes `+++ ` headers after `--- ` source headers. + - Confirmed the duplicate SDD command placeholder was removed instead of replaced with a nonexistent plan-update command. +- Addressed still-valid outside-diff findings: + - Replaced `specfact project --help` placeholders in `/specfact.03-review` with the current bundled `/specfact.03-review --list-questions`, `--list-findings`, and `--answers` prompt command surface. Local runtime confirms `specfact plan` is not mounted and `specfact project` has no review subcommand in this module contract. + - Added `specfact_govern.enforce.commands` and `specfact_spec.contract.commands` to docs command validation mounts, without reintroducing removed flat shims such as `specfact plan`. +- Validation: + - `hatch run format` -> passed. + - `hatch run generate-command-overview` -> passed. + - `hatch run pytest tests/unit/specfact_code_review/review/test_commands.py tests/unit/test_check_docs_commands_script.py tests/unit/test_check_prompt_commands_script.py -q` -> 48 passed. + - `hatch run check-command-overview` -> passed. + - `hatch run check-command-contract` -> passed: `check-command-contract: OK (86 generated module command path(s) validated)`. + - `hatch run python scripts/check-docs-commands.py` -> passed. + - `hatch run python scripts/check-prompt-commands.py` -> passed. + - `hatch run lint` -> passed. + - `openspec validate tester-module-cli-reliability --strict` -> passed. + +## Fifth Follow-up PR Review Fix + +- Re-checked the PR #307 review finding about ambiguous `specfact code review run` enforcement flags against current code. +- Finding status: valid. `--mode shadow --enforcement changed` could bypass the existing `_resolve_cli_enforcement` conflict check because `changed` is also the default. +- Fix: + - Added an early `_execute_review_run` guard that rejects deprecated `--mode` when Click reports `--enforcement` was explicitly supplied. + - Preserved legacy `--mode` compatibility when `--enforcement` is only defaulted. + - Added a regression test that confirms `run_command` is not called for the ambiguous flag combination. + - Added a `specfact-code-review-run` CLI contract anti-pattern scenario for `--mode shadow --enforcement changed`. + - Updated the existing blocking error-level report scenario to request `--enforcement full`, matching the intended `changed` default policy. +- Validation: + - `hatch run pytest tests/unit/specfact_code_review/review/test_commands.py -q` -> 15 passed. + - `hatch run validate-cli-contracts` -> passed: `Validated 3 CLI contract scenario files.` + - `hatch run pytest tests/integration/specfact_code_review/test_cli_contract_review_run_reports.py -q` -> 3 passed. + - `hatch run sign-modules --changed-only --bump-version patch --allow-unsigned --payload-from-filesystem` -> bumped `packages/specfact-code-review/module-package.yaml` from `0.47.40` to `0.47.41` and refreshed checksum. + - `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump` -> passed: `Verified 6 module manifest(s).` + - `hatch run pytest tests/unit/specfact_code_review/review/test_commands.py tests/integration/specfact_code_review/test_cli_contract_review_run_reports.py -q` -> 18 passed. + - `hatch run lint` -> passed. + - `hatch run python scripts/pre_commit_code_review.py packages/specfact-code-review/src/specfact_code_review/review/commands.py tests/unit/specfact_code_review/review/test_commands.py tests/cli-contracts/specfact-code-review-run.scenarios.yaml openspec/changes/tester-module-cli-reliability/TDD_EVIDENCE.md packages/specfact-code-review/module-package.yaml` -> passed with one info-only advisory on the pre-existing Typer `run` command length; left out of scope for this review-thread fix. + - `openspec validate tester-module-cli-reliability --strict` -> passed. diff --git a/openspec/changes/tester-module-cli-reliability/proposal.md b/openspec/changes/tester-module-cli-reliability/proposal.md new file mode 100644 index 00000000..7361c925 --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/proposal.md @@ -0,0 +1,43 @@ +## Why + +Primary tester reports filed in `nold-ai/specfact-cli#586` through `#592` expose module-owned CLI contract failures: project regeneration crashes on missing/null bundle data, sync bridge help still advertises removed flat commands, code import docs/help allow invalid option ordering, semgrep diagnostics miss the active uv environment, and backlog command groups report missing input without actionable help. + +Modules owns the runnable command implementations, module docs, prompt resources, and module command overview artifacts. Core owns the shared CLI error contract and package-manager runtime matrix in paired change `tester-cli-reliability`. + +## What Changes + +- Harden project, codebase, sync, and backlog command contracts against the tester-reported failures. +- Apply the shared CLI error contract in module command groups: missing subcommands and missing parameters show help plus the missing information. +- Generate deterministic module command overview artifacts for AI agents and docs validation. +- Replace module docs/prompt/template command validation allowlists that still accept legacy flat shims. +- Align semgrep/tool diagnostics with the active uv/hatch/pip/pipx execution context exposed by core helpers. + +## Capabilities + +### New Capabilities + +- `module-command-overview` +- `module-cli-error-contract` + +### Modified Capabilities + +- `modules-docs-command-validation` +- `backlog-delta` +- `code-review-tool-dependencies` + +## Impact + +- Affected packages: `specfact-project`, `specfact-codebase`, `specfact-backlog`, and command validation scripts. +- Affected docs/resources: module command docs, prompt resources, generated `llms.txt`, generated command reference Markdown/JSON, README links. +- Affected tests: project regenerate diagnostics, sync bridge help text, code import contract/migration error, semgrep active-env probing, backlog auth/delta status CLI behavior, generated command overview freshness. + +## Source Tracking + + +- **Parent Feature**: [#305](https://github.com/nold-ai/specfact-cli-modules/issues/305), plus existing module features [#161](https://github.com/nold-ai/specfact-cli-modules/issues/161), [#147](https://github.com/nold-ai/specfact-cli-modules/issues/147), and [#234](https://github.com/nold-ai/specfact-cli-modules/issues/234) +- **Change User Story**: [#306](https://github.com/nold-ai/specfact-cli-modules/issues/306) +- **Source Bugs**: [nold-ai/specfact-cli#586](https://github.com/nold-ai/specfact-cli/issues/586), [#587](https://github.com/nold-ai/specfact-cli/issues/587), [#588](https://github.com/nold-ai/specfact-cli/issues/588), [#589](https://github.com/nold-ai/specfact-cli/issues/589), [#590](https://github.com/nold-ai/specfact-cli/issues/590), [#591](https://github.com/nold-ai/specfact-cli/issues/591), [#592](https://github.com/nold-ai/specfact-cli/issues/592) +- **Paired Core Change**: `tester-cli-reliability`, tracked by [nold-ai/specfact-cli#594](https://github.com/nold-ai/specfact-cli/issues/594) +- **Repository**: nold-ai/specfact-cli-modules +- **Last Synced Status**: GitHub feature and story created; project/parent fields may need project-board field sync if CLI auth lacks project scope. +- **Sanitized**: false diff --git a/openspec/changes/tester-module-cli-reliability/specs/backlog-delta/spec.md b/openspec/changes/tester-module-cli-reliability/specs/backlog-delta/spec.md new file mode 100644 index 00000000..33306ca7 --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/specs/backlog-delta/spec.md @@ -0,0 +1,19 @@ +## MODIFIED Requirements + +### Requirement: Backlog delta status resolves documented defaults + +`backlog delta status` SHALL resolve project and repository inputs consistently with other backlog commands where configuration defaults are available. + +#### Scenario: Delta status uses configured GitHub repository defaults + +- **GIVEN** `.specfact/backlog-config.yaml` contains a default GitHub repository owner and name +- **WHEN** the user runs `specfact backlog delta status github` +- **THEN** the command resolves the repository owner and name from configuration +- **AND** it does not require undocumented repo parameters. + +#### Scenario: Delta status exposes missing repository inputs + +- **GIVEN** required repository inputs are not present in CLI arguments or configuration +- **WHEN** the user runs `specfact backlog delta status` +- **THEN** the command help and error output names the missing kebab-case options +- **AND** the command does not emit raw internal names such as `repo_owner` or `repo_name`. diff --git a/openspec/changes/tester-module-cli-reliability/specs/code-review-tool-dependencies/spec.md b/openspec/changes/tester-module-cli-reliability/specs/code-review-tool-dependencies/spec.md new file mode 100644 index 00000000..4a208bad --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/specs/code-review-tool-dependencies/spec.md @@ -0,0 +1,19 @@ +## MODIFIED Requirements + +### Requirement: Tool dependency diagnostics use active environment context + +Tool dependency checks SHALL probe the active uv, hatch, pip, or pipx execution context before reporting a tool as unavailable. + +#### Scenario: Semgrep available through uv is detected + +- **GIVEN** a project where `uv run semgrep --version` succeeds +- **WHEN** a codebase or code-review module checks semgrep availability +- **THEN** semgrep is reported as available +- **AND** the diagnostic does not tell the user to install semgrep with a pip-only command. + +#### Scenario: Missing tool hints match active manager + +- **GIVEN** a required tool is unavailable +- **WHEN** a module emits an installation hint +- **THEN** the hint matches the active manager context when known +- **AND** the output identifies which manager context was checked. diff --git a/openspec/changes/tester-module-cli-reliability/specs/module-cli-error-contract/spec.md b/openspec/changes/tester-module-cli-reliability/specs/module-cli-error-contract/spec.md new file mode 100644 index 00000000..101f5251 --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/specs/module-cli-error-contract/spec.md @@ -0,0 +1,37 @@ +## ADDED Requirements + +### Requirement: Module Commands Follow Shared CLI Error Contract + +Module command groups and leaf commands SHALL render actionable help for missing subcommands and missing required parameters. + +#### Scenario: Backlog auth without subcommand shows help and missing-subcommand guidance + +- **GIVEN** the user invokes `specfact backlog auth` +- **WHEN** no auth subcommand is provided +- **THEN** the output includes `backlog auth` help +- **AND** it states that a subcommand is required +- **AND** it lists provider/status/clear subcommands +- **AND** the command exits with a usage-error status. + +#### Scenario: Backlog delta status names missing required inputs + +- **GIVEN** the user invokes `specfact backlog delta status` without resolvable project or repository inputs +- **WHEN** required inputs cannot be resolved from CLI arguments or configuration +- **THEN** the output includes command help +- **AND** it names the missing CLI options using kebab-case option names +- **AND** it does not emit undocumented snake_case parameter names. + +#### Scenario: Code import legacy option ordering is actionable + +- **GIVEN** the user invokes `specfact code import --repo .` +- **WHEN** that ordering is not accepted by the command contract +- **THEN** the output includes help or migration guidance +- **AND** it shows the canonical supported invocation +- **AND** it does not report only `No such command '--repo'`. + +#### Scenario: Project regenerate null bundle data is typed + +- **GIVEN** project bundle processing encounters missing or null bundle data +- **WHEN** `specfact project regenerate` runs +- **THEN** the command reports a typed validation or bundle-data diagnostic +- **AND** it does not crash with a raw `NoneType` attribute error. diff --git a/openspec/changes/tester-module-cli-reliability/specs/module-command-overview/spec.md b/openspec/changes/tester-module-cli-reliability/specs/module-command-overview/spec.md new file mode 100644 index 00000000..d247eba7 --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/specs/module-command-overview/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Modules Publish Generated Command Overview Artifacts + +The modules repository SHALL generate deterministic command overview artifacts from the actual module command tree. + +#### Scenario: Module command overview artifacts are generated + +- **GIVEN** the module command overview generator runs in the modules repository +- **WHEN** it writes artifacts +- **THEN** it produces `llms.txt`, `docs/reference/commands.generated.md`, and `docs/reference/commands.generated.json` +- **AND** every command record includes command path, owning repo, owning module package, install prerequisite, short help, arguments/options, subcommands, source import path when known, and hidden/deprecated status +- **AND** generated output is stable for the same source tree. + +#### Scenario: README links generated overview + +- **GIVEN** a user or AI agent opens the modules repository README +- **WHEN** they look for command usage +- **THEN** the README links to the generated module command overview artifact. + +#### Scenario: Stale generated artifacts fail checks + +- **GIVEN** module command source, module manifests, prompt resources, docs, or command validation scripts change +- **WHEN** the command overview freshness check runs +- **THEN** it fails if generated artifacts are stale +- **AND** it reports the command needed to regenerate them. diff --git a/openspec/changes/tester-module-cli-reliability/specs/modules-docs-command-validation/spec.md b/openspec/changes/tester-module-cli-reliability/specs/modules-docs-command-validation/spec.md new file mode 100644 index 00000000..183e8bd9 --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/specs/modules-docs-command-validation/spec.md @@ -0,0 +1,26 @@ +## MODIFIED Requirements + +### Requirement: Module docs command examples are validated + +Module documentation command examples SHALL be validated against the generated module command overview. + +#### Scenario: Legacy flat sync command fails validation + +- **GIVEN** module docs, help examples, prompts, Jinja2 templates, YAML/JSON resources, or text guidance contain `specfact sync bridge` +- **WHEN** docs command validation runs +- **THEN** validation fails unless the reference is explicitly marked as historical migration material +- **AND** the finding identifies `specfact project sync bridge` as the canonical command when appropriate. + +#### Scenario: Prompt validators do not whitelist removed flat mounts + +- **GIVEN** a validator scans module prompt resources +- **WHEN** it builds the command contract +- **THEN** it uses generated module command overview data +- **AND** it does not accept removed flat mounts such as `specfact import`, `specfact sync`, `specfact plan`, or `specfact migrate` as canonical command groups. + +#### Scenario: Invalid option ordering fails validation + +- **GIVEN** docs or prompts contain `specfact code import --repo .` +- **WHEN** validation runs +- **THEN** the validator rejects the example if the command contract does not support that order +- **AND** the finding includes the canonical supported command form. diff --git a/openspec/changes/tester-module-cli-reliability/tasks.md b/openspec/changes/tester-module-cli-reliability/tasks.md new file mode 100644 index 00000000..f1190bfe --- /dev/null +++ b/openspec/changes/tester-module-cli-reliability/tasks.md @@ -0,0 +1,40 @@ +# Tasks: tester-module-cli-reliability + +## 1. Readiness and source tracking + +- [x] 1.1 Confirm tester bugs are mapped to module ownership and record the decision in `TDD_EVIDENCE.md`. +- [x] 1.2 Confirm GitHub Feature `#305`, User Story `#306`, and paired core story `nold-ai/specfact-cli#594` exist with source links and labels. +- [x] 1.3 Validate the OpenSpec change with `openspec validate tester-module-cli-reliability --strict`. + +## 2. Spec-first and failing evidence + +- [x] 2.1 Add spec deltas for module CLI error contract, command overview artifacts, docs/prompt command validation, backlog delta, and tool dependency probing. +- [x] 2.2 Add failing tests for project sync bridge help text, code import legacy ordering, backlog auth missing subcommand, backlog delta config/flags, generated command overview freshness, and prompt stale-command detection. +- [x] 2.3 Run targeted tests before production edits and record failing evidence in `TDD_EVIDENCE.md`. + +## 3. Module command contract fixes + +- [x] 3.1 Fix `project regenerate` to produce typed diagnostics when bundle data is missing or null. +- [x] 3.2 Replace flat `specfact sync bridge` help/docs/prompts with canonical `specfact project sync bridge`. +- [x] 3.3 Make `code import` help and errors unambiguous for supported option ordering and legacy migration guidance. +- [x] 3.4 Make direct module command groups inherit shared help plus missing-subcommand and missing-parameter guidance, including `backlog auth`. +- [x] 3.5 Make `backlog delta status` resolve documented config defaults consistently with `daily` and expose required repo/project inputs. + +## 4. Module command overview and validation + +- [x] 4.1 Add deterministic module command overview generation for `llms.txt`, Markdown, and JSON artifacts. +- [x] 4.2 Link the module command overview from `README.md`. +- [x] 4.3 Remove legacy flat mount whitelists from docs/prompt validators and add generated-contract freshness validation. +- [x] 4.4 Repair stale module docs, prompts, templates, and guidance strings. + +## 5. Runtime tool diagnostics and gates + +- [x] 5.1 Adopt active-context semgrep/tool probing for module diagnostics. +- [x] 5.2 Add CI/pre-commit freshness checks for generated module command artifacts. +- [x] 5.3 Coordinate with core package-manager runtime matrix for installed module smoke coverage. + +## 6. Passing evidence and quality gates + +- [x] 6.1 Re-run targeted tests and record passing evidence in `TDD_EVIDENCE.md`. +- [x] 6.2 Run required quality gates for touched scope: format, type-check, lint, YAML lint, contract-test, smart-test or targeted equivalent. +- [x] 6.3 Run SpecFact code review and resolve findings or document explicit exceptions. diff --git a/packages/specfact-backlog/module-package.yaml b/packages/specfact-backlog/module-package.yaml index efe230c5..e0a14eeb 100644 --- a/packages/specfact-backlog/module-package.yaml +++ b/packages/specfact-backlog/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-backlog -version: 0.41.25 +version: 0.41.28 commands: - backlog tier: official @@ -27,5 +27,5 @@ schema_extensions: project_metadata: - backlog_core.backlog_config integrity: - checksum: sha256:bd90c00d5b28c3c378f7824971bd5a8e870b5280e6837d447391816722cc6939 - signature: FIZD4ZyAfEM1SiuaxRNVfasEcpV6SaTZ+/7+mNJAvDNZzSdneC/DMiXXxTcHjVadkzLL6+7OHAEhuzXf5CZsCA== + checksum: sha256:7aee384a5e0b1b6cf1eaa54e259d35efab0fb38fb4498c171d2fe37b0d6d94c3 + signature: Sx6pQMGz1pjW3LAnuGtI2olvlX5OcetOOW5L4CWPx+RmsWQRLEgMqkonKUSiJMe90NXzgNvfqtqTPKB7gS5hCg== diff --git a/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md b/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md index cd7b0a2e..23231153 100644 --- a/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md @@ -111,13 +111,13 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: ## Available CLI Commands -- `specfact plan init ` - Initialize project bundle -- `specfact plan select ` - Set active plan (used as default for other commands) +- `specfact project --help` - Initialize project bundle +- `specfact project --help` - Set active plan (used as default for other commands) - `specfact code import [] --repo ` - Import from codebase (uses active plan if bundle not specified) -- `specfact plan review []` - Review plan (uses active plan if bundle not specified) -- `specfact plan harden []` - Create SDD manifest (uses active plan if bundle not specified) +- `specfact project --help` - Review plan (uses active plan if bundle not specified) +- `specfact project --help` - Create SDD manifest (uses active plan if bundle not specified) - `specfact govern enforce sdd []` - Validate SDD (uses active plan if bundle not specified) -- `specfact sync bridge --adapter --repo ` - Sync with external tools +- `specfact project sync bridge --adapter --repo ` - Sync with external tools - See [Command Reference](https://docs.specfact.io/reference/commands/) for full list **Note**: Most commands now support active plan fallback. If `--bundle` is not specified, commands automatically use the active plan set via `plan select`. This improves workflow efficiency in AI IDE environments. diff --git a/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md b/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md index acfc3a30..062abdc1 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md +++ b/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md @@ -524,12 +524,12 @@ Items updated in remote backlog: # Cross-adapter sync workflow: Refine GitHub → Sync to ADO (with state preservation) /specfact.backlog-refine --adapter github --repo-owner nold-ai --repo-name specfact-cli --write --labels feature # Then sync to ADO (state will be automatically mapped: open → New, closed → Closed) -# specfact sync bridge --adapter ado --ado-org my-org --ado-project my-project --mode bidirectional +# specfact project sync bridge --adapter ado --ado-org my-org --ado-project my-project --mode bidirectional # Cross-adapter sync workflow: Refine ADO → Sync to GitHub (with state preservation) /specfact.backlog-refine --adapter ado --ado-org my-org --ado-project my-project --write --state Active # Then sync to GitHub (state will be automatically mapped: New → open, Closed → closed) -# specfact sync bridge --adapter github --repo-owner my-org --repo-name my-repo --mode bidirectional +# specfact project sync bridge --adapter github --repo-owner my-org --repo-name my-repo --mode bidirectional ``` ## Troubleshooting diff --git a/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md b/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md index 4a3a8e71..77562aff 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md +++ b/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md @@ -156,7 +156,7 @@ Sanitized proposal description text. ```bash # GitHub adapter -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --no-sanitize --change-ids \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ @@ -164,7 +164,7 @@ specfact sync bridge --adapter github --mode export-only --repo [--github-token ] [--use-gh-cli] # Azure DevOps adapter -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --no-sanitize --change-ids \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ @@ -176,7 +176,7 @@ specfact sync bridge --adapter ado --mode export-only --repo \ ```bash # Step 3a: Export to temporary file for LLM review (GitHub) -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --sanitize --change-ids \ [--code-repo ] \ --export-to-tmp --tmp-file /tmp/specfact-proposal-.md \ @@ -184,7 +184,7 @@ specfact sync bridge --adapter github --mode export-only --repo [--github-token ] [--use-gh-cli] # Step 3a: Export to temporary file for LLM review (ADO) -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --sanitize --change-ids \ [--code-repo ] \ --export-to-tmp --tmp-file /tmp/specfact-proposal-.md \ @@ -232,14 +232,14 @@ specfact sync bridge --adapter ado --mode export-only --repo \ ```bash # Step 5a: Import sanitized content from temporary file (GitHub) -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --import-from-tmp --tmp-file /tmp/specfact-proposal--sanitized.md \ --change-ids \ [--target-repo ] [--repo-owner ] [--repo-name ] \ [--github-token ] [--use-gh-cli] # Step 5a: Import sanitized content from temporary file (ADO) -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --import-from-tmp --tmp-file /tmp/specfact-proposal--sanitized.md \ --change-ids \ --ado-org --ado-project \ @@ -310,13 +310,13 @@ When in copilot mode, follow this workflow: ```bash # For each sanitized proposal, export to temp file (GitHub) -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --change-ids --export-to-tmp --tmp-file /tmp/specfact-proposal-.md \ [--code-repo ] \ [--repo-owner ] [--repo-name ] [--github-token ] [--use-gh-cli] # For each sanitized proposal, export to temp file (ADO) -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --change-ids --export-to-tmp --tmp-file /tmp/specfact-proposal-.md \ [--code-repo ] \ --ado-org --ado-project [--ado-token ] [--ado-base-url ] @@ -387,14 +387,14 @@ specfact sync bridge --adapter ado --mode export-only --repo \ ```bash # Export non-sanitized proposals directly (GitHub) -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --change-ids --no-sanitize \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ [--repo-owner ] [--repo-name ] [--github-token ] [--use-gh-cli] # Export non-sanitized proposals directly (ADO) -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --change-ids --no-sanitize \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ @@ -413,14 +413,14 @@ specfact sync bridge --adapter ado --mode export-only --repo \ ```bash # For each approved sanitized proposal, import from temp file and create issue (GitHub) -specfact sync bridge --adapter github --mode export-only --repo \ +specfact project sync bridge --adapter github --mode export-only --repo \ --change-ids --import-from-tmp --tmp-file /tmp/specfact-proposal--sanitized.md \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ [--repo-owner ] [--repo-name ] [--github-token ] [--use-gh-cli] # For each approved sanitized proposal, import from temp file and create work item (ADO) -specfact sync bridge --adapter ado --mode export-only --repo \ +specfact project sync bridge --adapter ado --mode export-only --repo \ --change-ids --import-from-tmp --tmp-file /tmp/specfact-proposal--sanitized.md \ [--code-repo ] \ [--track-code-changes] [--add-progress-comment] \ diff --git a/packages/specfact-backlog/src/specfact_backlog/backlog/auth_commands.py b/packages/specfact-backlog/src/specfact_backlog/backlog/auth_commands.py index bec32bf0..c6861cec 100644 --- a/packages/specfact-backlog/src/specfact_backlog/backlog/auth_commands.py +++ b/packages/specfact-backlog/src/specfact_backlog/backlog/auth_commands.py @@ -30,10 +30,24 @@ DEFAULT_GITHUB_SCOPES = "repo read:project project" DEFAULT_GITHUB_CLIENT_ID = "Ov23lizkVHsbEIjZKvRD" -auth_app = typer.Typer(help="Authenticate backlog providers (Azure DevOps and GitHub)") +auth_app = typer.Typer( + help="Authenticate backlog providers (Azure DevOps and GitHub)", + invoke_without_command=True, + context_settings={"help_option_names": ["-h", "--help"]}, +) console = Console() +@auth_app.callback(invoke_without_command=True) +def auth_group(ctx: typer.Context) -> None: + """Authenticate backlog providers (Azure DevOps and GitHub).""" + if ctx.invoked_subcommand is not None: + return + typer.echo(ctx.get_help()) + typer.echo("\nError: Missing subcommand. Choose one of: azure-devops, github, status, clear.") + raise typer.Exit(2) + + @beartype @ensure(lambda result: isinstance(result, str), "Must return base URL") def _normalize_github_host(base_url: str) -> str: diff --git a/packages/specfact-backlog/src/specfact_backlog/backlog/commands.py b/packages/specfact-backlog/src/specfact_backlog/backlog/commands.py index afa0d549..775dc804 100644 --- a/packages/specfact-backlog/src/specfact_backlog/backlog/commands.py +++ b/packages/specfact-backlog/src/specfact_backlog/backlog/commands.py @@ -329,13 +329,12 @@ def _invoke_optional_ceremony_delegate( raise typer.Exit(code=2) -@beartype @ceremony_app.command( "standup", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def ceremony_standup( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), mode: str = typer.Option("scrum", "--mode", help="Ceremony mode (default: scrum)"), ) -> None: @@ -344,26 +343,24 @@ def ceremony_standup( _invoke_backlog_subcommand("daily", [*forwarded, *ctx.args]) -@beartype @ceremony_app.command( "refinement", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def ceremony_refinement( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), ) -> None: """Ceremony alias for `backlog refine`.""" _invoke_backlog_subcommand("refine", [adapter, *ctx.args]) -@beartype @ceremony_app.command( "planning", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def ceremony_planning( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), mode: str = typer.Option("scrum", "--mode", help="Ceremony mode (default: scrum)"), ) -> None: @@ -373,13 +370,12 @@ def ceremony_planning( _invoke_optional_ceremony_delegate([delegate], [*forwarded, *ctx.args], ceremony_name="planning") -@beartype @ceremony_app.command( "flow", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def ceremony_flow( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), mode: str = typer.Option("kanban", "--mode", help="Ceremony mode (default: kanban)"), ) -> None: @@ -389,13 +385,12 @@ def ceremony_flow( _invoke_optional_ceremony_delegate([delegate], [*forwarded, *ctx.args], ceremony_name="flow") -@beartype @ceremony_app.command( "pi-summary", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) def ceremony_pi_summary( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), mode: str = typer.Option("safe", "--mode", help="Ceremony mode (default: safe)"), ) -> None: @@ -2843,11 +2838,10 @@ def _read_refined_content_from_stdin() -> str: # fmt: off -@beartype @app.command() @require(lambda adapter: isinstance(adapter, str) and len(adapter) > 0, "Adapter must be non-empty string") def daily( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), assignee: str | None = typer.Option(None, "--assignee", help="Filter by assignee (e.g. 'me' or username). Use 'any' to disable assignee filtering."), @@ -3312,11 +3306,10 @@ def _render_daily_patch_preview(options: dict[str, Any], state: dict[str, Any]) app.add_typer(_delta_app, name="delta", help="Backlog delta analysis and impact tracking") -@beartype @app.command() @require(lambda adapter: isinstance(adapter, str) and len(adapter) > 0, "Adapter must be non-empty string") def refine( - ctx: click.Context, + ctx: typer.Context, adapter: str = typer.Argument(..., help="Backlog adapter name (github, ado, etc.)"), labels: list[str] | None = typer.Option( None, "--labels", "--tags", help="Filter by labels/tags (can specify multiple)" @@ -4104,9 +4097,8 @@ def init_config( @app.command("map-fields") -@beartype def map_fields( - ctx: click.Context, + ctx: typer.Context, ado_org: str | None = typer.Option(None, "--ado-org", help="Azure DevOps organization"), ado_project: str | None = typer.Option(None, "--ado-project", help="Azure DevOps project"), ado_token: str | None = typer.Option(None, "--ado-token", help="Azure DevOps PAT"), diff --git a/packages/specfact-backlog/src/specfact_backlog/backlog_core/commands/delta.py b/packages/specfact-backlog/src/specfact_backlog/backlog_core/commands/delta.py index a829ee32..1c084b75 100644 --- a/packages/specfact-backlog/src/specfact_backlog/backlog_core/commands/delta.py +++ b/packages/specfact-backlog/src/specfact_backlog/backlog_core/commands/delta.py @@ -7,6 +7,7 @@ from typing import Annotated, Any import typer +import yaml from beartype import beartype from rich.console import Console from rich.table import Table @@ -57,6 +58,54 @@ def _empty_delta() -> dict[str, Any]: } +def _load_provider_config(adapter: str) -> dict[str, Any]: + config_path = Path(".specfact") / "backlog-config.yaml" + if not config_path.exists(): + return {} + try: + loaded = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError): + return {} + if not isinstance(loaded, dict): + return {} + providers = loaded.get("providers") + if isinstance(providers, dict) and isinstance(providers.get(adapter), dict): + return dict(providers[adapter]) + direct = loaded.get(adapter) + if isinstance(direct, dict): + return dict(direct) + return {} + + +def _resolve_project_id( + *, + adapter: str, + project_id: str | None, + repo_owner: str | None, + repo_name: str | None, +) -> str | None: + provider_config = _load_provider_config(adapter) + configured_project = provider_config.get("project_id") + if project_id: + return project_id + if isinstance(configured_project, str) and configured_project.strip(): + return configured_project.strip() + owner = repo_owner or provider_config.get("repo_owner") + name = repo_name or provider_config.get("repo_name") + if adapter == "github" and isinstance(owner, str) and isinstance(name, str) and owner and name: + return f"{owner}/{name}" + return None + + +def _exit_missing_delta_context(ctx: typer.Context) -> None: + typer.echo(ctx.get_help()) + typer.echo( + "\nError: Missing backlog context. Provide --project-id, or for GitHub provide " + "--repo-owner and --repo-name, or configure .specfact/backlog-config.yaml." + ) + raise typer.Exit(2) + + @beartype def _render_delta_table(delta: dict[str, Any], title: str = "Delta Status") -> None: table = Table(title=title) @@ -71,10 +120,13 @@ def _render_delta_table(delta: dict[str, Any], title: str = "Delta Status") -> N console.print(table) -@beartype def status( - project_id: Annotated[str, typer.Option("--project-id", help="Backlog project identifier")], + ctx: typer.Context, + adapter_arg: Annotated[str | None, typer.Argument(help="Adapter to use")] = None, + project_id: Annotated[str | None, typer.Option("--project-id", help="Backlog project identifier")] = None, adapter: Annotated[str, typer.Option("--adapter", help="Adapter to use")] = "github", + repo_owner: Annotated[str | None, typer.Option("--repo-owner", help="GitHub repository owner")] = None, + repo_name: Annotated[str | None, typer.Option("--repo-name", help="GitHub repository name")] = None, since: Annotated[str | None, typer.Option("--since", help="ISO timestamp filter")] = None, baseline_file: Annotated[Path, typer.Option("--baseline-file", help="Path to baseline graph JSON")] = Path( ".specfact/backlog-baseline.json" @@ -82,8 +134,18 @@ def status( template: Annotated[str, typer.Option("--template", help="Template name for mapping")] = "github_projects", ) -> None: """Show backlog delta status compared to baseline.""" + effective_adapter = adapter_arg or adapter + effective_project_id = _resolve_project_id( + adapter=effective_adapter, + project_id=project_id, + repo_owner=repo_owner, + repo_name=repo_name, + ) + if not effective_project_id: + _exit_missing_delta_context(ctx) + assert effective_project_id is not None baseline_graph = _load_baseline_graph(baseline_file) - current_graph = _fetch_current_graph(project_id, adapter, template) + current_graph = _fetch_current_graph(effective_project_id, effective_adapter, template) delta = compute_delta(baseline_graph, current_graph) if since is not None: diff --git a/packages/specfact-code-review/module-package.yaml b/packages/specfact-code-review/module-package.yaml index 6ba44a94..46458251 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.34 +version: 0.47.41 commands: - code tier: official @@ -23,5 +23,5 @@ description: Official SpecFact code review bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:a82faca89f091d03ebf0692377dd33f830fc11c15e104c0faeccbfffc66aa7f0 - signature: KbRpwF9Kj9/Eqb8AuGaGzjXiKUoKXEfsI49pHPHsgs++6Ps29RIsdLcuSb0ckRZLSyUApYWzuG2einzSbOIvCw== + checksum: sha256:431742190e75c36389c3c4d769a374fe387df2967310f28c78b1da5115849680 + signature: 9D5X4OMBhIqJvM3hlNMTCbLquK44xVGTgpB3ke0/iCeY2YwC/0/fcKZ9atIOM9mZMRU4ca+YF8bRQJZyHPiKAA== diff --git a/packages/specfact-code-review/src/specfact_code_review/resources/skills/specfact-code-review/SKILL.md b/packages/specfact-code-review/src/specfact_code_review/resources/skills/specfact-code-review/SKILL.md index d28ec148..b13fb4f6 100644 --- a/packages/specfact-code-review/src/specfact_code_review/resources/skills/specfact-code-review/SKILL.md +++ b/packages/specfact-code-review/src/specfact_code_review/resources/skills/specfact-code-review/SKILL.md @@ -6,9 +6,10 @@ allowed-tools: [] # SpecFact Code Review Skill Updated: 2026-05-22 | Module: nold-ai/specfact-code-review Use this skill as an interactive cleanup coach, not a raw lint executor. When a user says "remove AI bloat", "simplify", "apply clean code", "fix SpecFact review", or similar, run the SpecFact review workflow, explain decisions in the user's language, show exact patch previews, and validate after small changes. +Operating guidance: command examples in this skill are not the source of truth; CLI help is authoritative. Check `specfact code review run --help`, and ask the user before guessing when help output disagrees. ## DO - Treat `specfact code review run --help` as authoritative; use `--instructions` as the fallback AI workflow when prompts/skills are unavailable -- For simplification queues, run `specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json` +- For simplification queues, run `specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json` - Inspect `cleanup_forecast` first, then treat each finding's `remediation_packet` as the portable AI IDE contract - Preserve anything with `preserve_reasons`; those reasons block automatic cleanup even when a shorter patch exists - Ask for walkthrough level when interactive; for vibe coders, present each finding as a decision card with issue, keep reason, patch preview, validation plan, and recommendation @@ -17,7 +18,7 @@ Use this skill as an interactive cleanup coach, not a raw lint executor. When a - Log each simplification action as recommended, applied, kept, skipped, failed, with evidence of improvement or preserved contract - In headless mode, process one file at a time and emit an action table: file, line, rule, guidance_kind, recommended_action, action_status, evidence - Run targeted tests or rerun simplify review after each accepted file or very small batch; if validation cannot prove safety, downgrade to `needs_tests` or `skipped` -- For merge-quality review, run `specfact code review run --scope changed --bug-hunt --json --out .specfact/code-review.json` +- For merge-quality review, run `specfact code review run --scope changed --enforcement changed --bug-hunt --json --out .specfact/code-review.json`; use `--enforcement full` only when the user wants legacy blockers in reviewed files to fail - Use intention-revealing names; avoid placeholder public names like data/process/handle - Keep functions under 120 LOC, shallow nesting, and <= 5 parameters (KISS) - Delete unused private helpers and speculative abstractions quickly (YAGNI) diff --git a/packages/specfact-code-review/src/specfact_code_review/review/commands.py b/packages/specfact-code-review/src/specfact_code_review/review/commands.py index 8440ba99..eec30886 100644 --- a/packages/specfact-code-review/src/specfact_code_review/review/commands.py +++ b/packages/specfact-code-review/src/specfact_code_review/review/commands.py @@ -1,4 +1,9 @@ -"""Review subgroup wiring for the code command surface.""" +"""Review subgroup wiring for the code command surface. + +Operating guidance: command examples in this source are not the source of +truth; CLI help is authoritative. Check `specfact code review run --help`, +and ask the user before guessing when help output disagrees. +""" from __future__ import annotations @@ -17,6 +22,7 @@ InvalidOptionCombinationError, MissingOutForJsonError, NoReviewableFilesError, + ReviewRunMode, RunCommandError, run_command, ) @@ -31,7 +37,7 @@ Use this when the user asks to remove AI bloat, simplify code, apply clean-code patterns, reduce boilerplate, or act on SpecFact review findings. 1. Generate evidence first: - specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review.json + specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review.json Keep the canonical .specfact/code-review.json path unless every downstream consumer has been updated to read a custom simplify report path. @@ -53,7 +59,7 @@ 5. For design_judgment findings, check API, callback, framework hook, adapter, public symbol, CLI boundary, compatibility shim, and readability intent. If intent is unclear, default to keep or skip. 6. Apply one file at a time. After each accepted file or very small batch, run targeted tests or rerun: - specfact code review run --scope changed --focus simplify --json --out .specfact/code-review.json + specfact code review run --scope changed --enforcement shadow --focus simplify --json --out .specfact/code-review.json 7. Log every action as recommended, applied, kept, skipped, or failed with evidence. Never batch-apply design_judgment findings just because the patch is shorter. Never treat ai_bloat findings as proof of AI authorship; they are cleanup signals only, not proof of AI authorship. """ @@ -70,6 +76,31 @@ class _ReviewRunCliInputs: interactive: bool +@dataclass(frozen=True) +class _ReviewRunCommandInputs: + ctx: typer.Context + files: list[Path] | None + scope: Literal["changed", "full"] | None + path: list[Path] | None + include_tests: bool | None + exclude_tests: bool | None + focus: list[str] | None + enforcement: ReviewRunMode + mode: Literal["shadow", "enforce"] | None + level: Literal["error", "warning"] | None + bug_hunt: bool + include_noise: bool + suppress_noise: bool + json_output: bool + out: Path | None + score_only: bool + no_tests: bool + fix: bool + preview_fixes: bool + with_mutation: bool + interactive: bool + + def _friendly_run_command_error(exc: RunCommandError | ValueError | ViolationError) -> str: if isinstance( exc, @@ -122,6 +153,82 @@ def _resolve_review_run_flags(inputs: _ReviewRunCliInputs) -> tuple[list[str], b return focus_list, resolved_include_tests, inputs.include_noise and not inputs.suppress_noise +def _resolve_cli_enforcement( + *, enforcement: ReviewRunMode, legacy_mode: Literal["shadow", "enforce"] | None +) -> ReviewRunMode: + """Resolve new enforcement policy with backward-compatible --mode support.""" + if legacy_mode is None: + return enforcement + if enforcement != "changed": + raise typer.BadParameter("Use either --enforcement or deprecated --mode, not both.") + return "shadow" if legacy_mode == "shadow" else "full" + + +def _enforcement_was_defaulted(ctx: typer.Context) -> bool: + """Return whether Click supplied the default --enforcement value.""" + get_parameter_source = getattr(ctx, "get_parameter_source", None) + if not callable(get_parameter_source): + return False + source = get_parameter_source("enforcement") + return getattr(source, "name", None) == "DEFAULT" + + +def _enforcement_was_explicit(ctx: typer.Context) -> bool: + """Return whether the caller supplied --enforcement explicitly.""" + get_parameter_source = getattr(ctx, "get_parameter_source", None) + if not callable(get_parameter_source): + return False + source = get_parameter_source("enforcement") + return source is not None and getattr(source, "name", None) != "DEFAULT" + + +def _execute_review_run(inputs: _ReviewRunCommandInputs) -> None: + if inputs.mode is not None and _enforcement_was_explicit(inputs.ctx): + raise typer.BadParameter("Use only one of --mode or --enforcement; --mode is deprecated.") + if inputs.mode is None and inputs.enforcement == "changed" and _enforcement_was_defaulted(inputs.ctx): + typer.echo( + "Code review enforcement default is 'changed'; use '--enforcement full' for strict CI gates " + "or '--enforcement shadow' for evidence-only runs.", + err=True, + ) + focus_list, resolved_include_tests, resolved_include_noise = _resolve_review_run_flags( + _ReviewRunCliInputs( + files=inputs.files, + include_tests=inputs.include_tests, + exclude_tests=inputs.exclude_tests, + focus=inputs.focus, + include_noise=inputs.include_noise, + suppress_noise=inputs.suppress_noise, + interactive=inputs.interactive, + ) + ) + + try: + exit_code, output = run_command( + inputs.files or [], + include_tests=resolved_include_tests, + scope=inputs.scope, + path_filters=inputs.path, + focus_facets=tuple(focus_list), + review_mode=_resolve_cli_enforcement(enforcement=inputs.enforcement, legacy_mode=inputs.mode), + review_level=inputs.level, + bug_hunt=inputs.bug_hunt, + include_noise=resolved_include_noise, + json_output=inputs.json_output, + out=inputs.out, + score_only=inputs.score_only, + no_tests=inputs.no_tests, + fix=inputs.fix, + preview_fixes=inputs.preview_fixes, + with_mutation=inputs.with_mutation, + ) + except (ValueError, ViolationError) as exc: + raise typer.BadParameter(_friendly_run_command_error(exc)) from exc + if output is not None: + typer.echo(output) + raise typer.Exit(code=exit_code) + + @review_app.command("run") @require(lambda ctx: True, "run command validation") @ensure(lambda result: result is None, "run command does not return") @@ -137,7 +244,16 @@ def run( "--focus", help="Limit to source, tests, docs, and/or simplify (repeatable).", ), - mode: Literal["shadow", "enforce"] = typer.Option("enforce", "--mode"), + enforcement: ReviewRunMode = typer.Option( + "changed", + "--enforcement", + help="Enforcement policy: full blocks all findings; changed blocks changed-line findings; shadow reports only.", + ), + mode: Literal["shadow", "enforce"] | None = typer.Option( + None, + "--mode", + help="Deprecated alias: enforce maps to --enforcement full; shadow maps to --enforcement shadow.", + ), level: Literal["error", "warning"] | None = typer.Option(None, "--level"), bug_hunt: bool = typer.Option(False, "--bug-hunt"), include_noise: bool = typer.Option(False, "--include-noise"), @@ -169,29 +285,21 @@ def run( if instructions: typer.echo(_RUN_INSTRUCTIONS) raise typer.Exit(code=0) - focus_list, resolved_include_tests, resolved_include_noise = _resolve_review_run_flags( - _ReviewRunCliInputs( + _execute_review_run( + _ReviewRunCommandInputs( + ctx=ctx, files=files, + scope=scope, + path=path, include_tests=include_tests, exclude_tests=exclude_tests, focus=focus, + enforcement=enforcement, + mode=mode, + level=level, + bug_hunt=bug_hunt, include_noise=include_noise, suppress_noise=suppress_noise, - interactive=interactive, - ) - ) - - try: - exit_code, output = run_command( - files or [], - include_tests=resolved_include_tests, - scope=scope, - path_filters=path, - focus_facets=tuple(focus_list), - review_mode=mode, - review_level=level, - bug_hunt=bug_hunt, - include_noise=resolved_include_noise, json_output=json_output, out=out, score_only=score_only, @@ -199,12 +307,9 @@ def run( fix=fix, preview_fixes=preview_fixes, with_mutation=with_mutation, + interactive=interactive, ) - except (ValueError, ViolationError) as exc: - raise typer.BadParameter(_friendly_run_command_error(exc)) from exc - if output is not None: - typer.echo(output) - raise typer.Exit(code=exit_code) + ) review_app.add_typer(ledger_app, name="ledger") diff --git a/packages/specfact-code-review/src/specfact_code_review/run/__init__.py b/packages/specfact-code-review/src/specfact_code_review/run/__init__.py index dd37c658..4d54cc5e 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/__init__.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/__init__.py @@ -26,7 +26,7 @@ def run_review( progress_callback: Callable[[str], None] | None = None, bug_hunt: bool = False, review_level: Literal["error", "warning"] | None = None, - review_mode: Literal["shadow", "enforce"] = "enforce", + review_mode: Literal["full", "changed", "shadow", "enforce"] = "full", ) -> ReviewReport: """Lazily import the orchestrator to avoid package import cycles.""" run_review_impl = import_module("specfact_code_review.run.runner").run_review diff --git a/packages/specfact-code-review/src/specfact_code_review/run/commands.py b/packages/specfact-code-review/src/specfact_code_review/run/commands.py index 02a5e8ca..c3aadbfe 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/commands.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/commands.py @@ -1,4 +1,9 @@ -"""Command implementation for `specfact code review run`.""" +"""Command implementation for `specfact code review run`. + +Operating guidance: command examples in this source are not the source of +truth; CLI help is authoritative. Check `specfact code review run --help`, +and ask the user before guessing when help output disagrees. +""" from __future__ import annotations @@ -27,7 +32,8 @@ console = Console() progress_console = Console(stderr=True) AutoScope = Literal["changed", "full"] -ReviewRunMode = Literal["shadow", "enforce"] +ReviewRunMode = Literal["full", "changed", "shadow"] +LegacyReviewRunMode = Literal["shadow", "enforce"] ReviewLevelFilter = Literal["error", "warning"] @@ -70,7 +76,7 @@ class ReviewRunRequest: preview_fixes: bool = False with_mutation: bool = False bug_hunt: bool = False - review_mode: ReviewRunMode = "enforce" + review_mode: ReviewRunMode = "changed" review_level: ReviewLevelFilter | None = None focus_facets: tuple[str, ...] = () review_focus: ReviewFocus | None = None @@ -332,7 +338,7 @@ def _with_applied_simplification_findings(report: ReviewReport, applied_findings def _with_simplify_enforce_verdict(report: ReviewReport, flags: _ReviewLoopFlags) -> ReviewReport: if ( flags.review_focus == "simplify" - and flags.review_mode == "enforce" + and flags.review_mode == "full" and report.simplification_summary is not None and report.simplification_summary.blocking_simplification_count > 0 ): @@ -774,10 +780,12 @@ def _as_optional_path(value: object) -> Path | None: def _as_review_mode(value: object) -> ReviewRunMode: - if value is None or value == "enforce": - return "enforce" - if value == "shadow": - return "shadow" + if value is None: + return "changed" + if value == "enforce": + return "full" + if value in ("full", "changed", "shadow"): + return cast(ReviewRunMode, value) raise RunCommandError(f"Invalid review mode: {value!r}") @@ -866,7 +874,7 @@ def _get_optional_param(name: str, validator: Callable[[object], object], defaul preview_fixes=_get_bool_param("preview_fixes"), with_mutation=_get_bool_param("with_mutation"), bug_hunt=_get_bool_param("bug_hunt"), - review_mode=_as_review_mode(request_kwargs.pop("review_mode", "enforce")), + review_mode=_as_review_mode(request_kwargs.pop("review_mode", "changed")), review_level=_as_review_level(request_kwargs.pop("review_level", None)), focus_facets=focus_facets, review_focus=_review_focus_from_facets(focus_facets), diff --git a/packages/specfact-code-review/src/specfact_code_review/run/findings.py b/packages/specfact-code-review/src/specfact_code_review/run/findings.py index 3bad107f..e15bfaf3 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/findings.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/findings.py @@ -473,6 +473,14 @@ class ReviewReport(BaseModel): default=None, description="Aggregate cleanup forecast for simplify-focused review runs.", ) + enforcement_mode: Literal["full", "changed", "shadow"] | None = Field( + default=None, + description="Review enforcement mode applied to the CI exit code.", + ) + enforcement_summary: str | None = Field( + default=None, + description="Human-readable explanation of enforcement mode and blocking evidence.", + ) house_rules_updates: list[str] = Field(default_factory=list, description="Suggested house-rules updates.") @field_validator("schema_version", "run_id", "summary") @@ -493,7 +501,9 @@ def _normalize_timestamp(cls, value: datetime) -> datetime: def _derive_governance_fields(self) -> ReviewReport: if self.simplification_summary is None: self.simplification_summary = _build_simplification_summary(self.findings) - if self.cleanup_forecast is not None or any( + if self.enforcement_mode is not None: + self.schema_version = "1.4" + elif self.cleanup_forecast is not None or any( finding.has_cleanup_handoff_metadata() for finding in self.findings ): self.schema_version = "1.3" diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runner.py b/packages/specfact-code-review/src/specfact_code_review/run/runner.py index 1ebfdecf..b14ab279 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/runner.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/runner.py @@ -67,6 +67,7 @@ _CLEAN_CODE_CONTEXT_HINTS = ("clean code", "naming", "kiss", "yagni", "dry", "solid", "complexity") _TARGETED_TEST_TIMEOUT = int(os.environ.get("SPECFACT_CODE_REVIEW_TARGETED_TEST_TIMEOUT", "120")) ReviewFocus = Literal["simplify"] +ReviewEnforcementMode = Literal["full", "changed", "shadow"] @dataclass(frozen=True) @@ -78,7 +79,7 @@ class ReviewOptions: progress_callback: Callable[[str], None] | None = None bug_hunt: bool = False review_level: Literal["error", "warning"] | None = None - review_mode: Literal["shadow", "enforce"] = "enforce" + review_mode: ReviewEnforcementMode = "full" focus: ReviewFocus | None = None @@ -227,6 +228,126 @@ def _is_test_file(file_path: str | Path) -> bool: return "tests" in Path(file_path).parts +def _normalize_report_path(raw_path: str | Path) -> str: + path = Path(raw_path) + return path.as_posix() + + +def _parse_added_lines_from_diff(diff_text: str) -> dict[str, set[int]]: + """Return added new-file line numbers from a zero-context git diff.""" + changed_lines: dict[str, set[int]] = {} + current_file: str | None = None + for line in diff_text.splitlines(): + if line.startswith("+++ "): + destination = line[4:].strip() + current_file = None if destination == "/dev/null" else destination.removeprefix("b/") + if current_file is not None: + changed_lines.setdefault(current_file, set()) + continue + if current_file is None or not line.startswith("@@ "): + continue + match = re.search(r"\+(\d+)(?:,(\d+))?", line) + if match is None: + continue + start = int(match.group(1)) + count = int(match.group(2) or "1") + if count > 0: + changed_lines[current_file].update(range(start, start + count)) + return changed_lines + + +def _changed_lines_from_git(files: list[Path]) -> dict[str, set[int]]: + """Collect changed line numbers for changed enforcement evidence.""" + diff_mode = os.environ.get("SPECFACT_CODE_REVIEW_CHANGED_DIFF", "worktree").strip().lower() + command = ["git", "diff", "--unified=0", "--no-ext-diff"] + if diff_mode == "cached": + command.append("--cached") + else: + command.append("HEAD") + if files: + command.extend(["--", *(str(file_path) for file_path in files)]) + result = subprocess.run(command, capture_output=True, text=True, check=False, timeout=30) + if result.returncode != 0: + return {} + changed_lines = _parse_added_lines_from_diff(result.stdout) + for file_path in files: + if not file_path.exists(): + continue + relative = _normalize_report_path(file_path) + if relative in changed_lines: + continue + try: + listed = subprocess.run( + ["git", "ls-files", "--others", "--exclude-standard", "--", str(file_path)], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + except subprocess.SubprocessError: + continue + if listed.returncode == 0 and listed.stdout.strip(): + try: + line_count = len(file_path.read_text(encoding="utf-8").splitlines()) + except (OSError, UnicodeDecodeError): + continue + changed_lines[relative] = set(range(1, line_count + 1)) + return changed_lines + + +def _finding_targets_changed_line(finding: ReviewFinding, changed_lines: dict[str, set[int]]) -> bool: + """Return whether a finding points at a changed line.""" + line_numbers = changed_lines.get(_normalize_report_path(finding.file)) + if not line_numbers: + return False + return finding.line in line_numbers + + +def _with_enforcement(report: ReviewReport, *, mode: ReviewEnforcementMode, files: list[Path]) -> ReviewReport: + """Apply enforcement mode to report exit code while preserving all findings as evidence.""" + if mode == "full": + return report.model_copy( + update={ + "enforcement_mode": "full", + "enforcement_summary": "Full enforcement blocks on any blocking finding in the reviewed files.", + } + ) + if mode == "shadow": + return report.model_copy( + update={ + "ci_exit_code": 0, + "enforcement_mode": "shadow", + "enforcement_summary": "Shadow enforcement records findings as evidence and never blocks CI.", + } + ) + changed_lines = _changed_lines_from_git(files) + blocking_changed = [ + finding + for finding in report.findings + if finding.is_blocking() and _finding_targets_changed_line(finding, changed_lines) + ] + if blocking_changed: + summary = f"Changed enforcement blocks on {len(blocking_changed)} blocking finding(s) on changed lines." + return report.model_copy( + update={"ci_exit_code": 1, "enforcement_mode": "changed", "enforcement_summary": summary} + ) + legacy_blocking = sum(finding.is_blocking() for finding in report.findings) + summary = ( + "Changed enforcement found no blocking findings on changed lines." + if legacy_blocking == 0 + else f"Changed enforcement found no blocking findings on changed lines; {legacy_blocking} legacy blocking finding(s) remain as evidence." + ) + verdict = "PASS" if not report.findings else "PASS_WITH_ADVISORY" + return report.model_copy( + update={ + "overall_verdict": verdict, + "ci_exit_code": 0, + "enforcement_mode": "changed", + "enforcement_summary": summary, + } + ) + + def _suppress_known_noise(findings: list[ReviewFinding]) -> list[ReviewFinding]: filtered: list[ReviewFinding] = [] for finding in findings: @@ -865,9 +986,11 @@ def _review_options_from_kwargs(options: ReviewOptions | None, overrides: dict[s review_level = overrides.get("review_level") if review_level not in {"error", "warning", None}: raise TypeError("review_level must be one of error, warning, or None") - review_mode = overrides.get("review_mode", "enforce") - if review_mode not in {"shadow", "enforce"}: - raise TypeError("review_mode must be one of shadow or enforce") + review_mode = overrides.get("review_mode", "full") + if review_mode not in {"full", "changed", "shadow", "enforce"}: + raise TypeError("review_mode must be one of full, changed, shadow, or enforce") + if review_mode == "enforce": + review_mode = "full" focus = overrides.get("focus") if focus not in {"simplify", None}: raise TypeError("focus must be simplify or None") @@ -877,7 +1000,7 @@ def _review_options_from_kwargs(options: ReviewOptions | None, overrides: dict[s progress_callback=cast(Callable[[str], None] | None, progress_callback), bug_hunt=bug_hunt, review_level=cast(Literal["error", "warning"] | None, review_level), - review_mode=cast(Literal["shadow", "enforce"], review_mode), + review_mode=cast(ReviewEnforcementMode, review_mode), focus=cast(ReviewFocus | None, focus), ) @@ -962,10 +1085,10 @@ def run_review( summary=_summary_for_findings(findings), cleanup_forecast=cleanup_forecast, ) - if review_options.review_mode == "shadow": - return report.model_copy(update={"ci_exit_code": 0}) + report = _with_enforcement(report, mode=review_options.review_mode, files=files) if ( review_options.focus == "simplify" + and review_options.review_mode == "full" and report.simplification_summary is not None and report.simplification_summary.blocking_simplification_count > 0 ): diff --git a/packages/specfact-codebase/module-package.yaml b/packages/specfact-codebase/module-package.yaml index b50f0036..61fd8133 100644 --- a/packages/specfact-codebase/module-package.yaml +++ b/packages/specfact-codebase/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-codebase -version: 0.41.10 +version: 0.41.11 commands: - code tier: official @@ -24,5 +24,5 @@ description: Official SpecFact codebase bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:f1a87d09b50ae91fd2e57986a04855796d658c704520b7d0889ccd4a00ad7b9a - signature: iVUCHYPzuJEIDOwPNmFZLEaAGmYj7E4vgLAT2N5SWwi+Jvzl5jsLgW86tbiChvKX1lwUrZtAS0sofZ2TwxVlDA== + checksum: sha256:21d8bc02de6e01ecc0b7d68c2ba9f736f90f97b128eb925aca721dbd02874c74 + signature: H1rUcKFH31LleuADUIuLwghMtHwtKWp+4o5cCZbObumSil9c27bIrdH6u8K04AYChea0vHZld4Pr7S2oIDBZBQ== diff --git a/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md b/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md index c393f272..2cb5e672 100644 --- a/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md @@ -111,13 +111,13 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: ## Available CLI Commands -- `specfact plan init ` - Initialize project bundle -- `specfact plan select ` - Set active plan (used as default for other commands) +- `specfact project --help` - Initialize project bundle +- `specfact project --help` - Set active plan (used as default for other commands) - `specfact code import [] --repo ` - Import from codebase (uses active plan if bundle not specified) -- `specfact plan review []` - Review plan (uses active plan if bundle not specified) -- `specfact plan harden []` - Create SDD manifest (uses active plan if bundle not specified) +- `specfact project --help` - Review plan (uses active plan if bundle not specified) +- `specfact project --help` - Create SDD manifest (uses active plan if bundle not specified) - `specfact govern enforce sdd []` - Validate SDD (uses active plan if bundle not specified) -- `specfact sync bridge --adapter --repo ` - Sync with external tools +- `specfact project sync bridge --adapter --repo ` - Sync with external tools - See [Command Reference](../../docs/reference/commands.md) for full list **Note**: Most commands now support active plan fallback. If `--bundle` is not specified, commands automatically use the active plan set via `plan select`. This improves workflow efficiency in AI IDE environments. diff --git a/packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py b/packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py index 01a590aa..ee2ada29 100644 --- a/packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py +++ b/packages/specfact-codebase/src/specfact_codebase/import_cmd/commands.py @@ -4,18 +4,66 @@ from pathlib import Path +import click import typer -from beartype import beartype from icontract import require +from typer.core import TyperGroup from specfact_project.import_cmd.commands import from_bridge as legacy_from_bridge, from_code as legacy_from_code +class _ImportCommandGroup(TyperGroup): + """Detect common legacy callback ordering and print a migration-quality hint.""" + + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if args and args[0] in self.commands: + if any(arg in ("--help", "-h", "--help-advanced", "-ha") for arg in args[1:]): + command = self.commands[args[0]] + try: + command.main( + args=args[1:], + prog_name=f"{ctx.command_path} {args[0]}", + standalone_mode=False, + ) + except click.exceptions.Exit as exc: + ctx.exit(exc.exit_code) + ctx.exit(0) + return self._parse_explicit_subcommand_args(ctx, args) + if args and args[0] not in self.commands and any(arg.startswith("-") for arg in args[1:]): + self._raise_legacy_order_error(ctx) + return super().parse_args(ctx, args) + + def resolve_command( + self, ctx: click.Context, args: list[str] + ) -> tuple[str | None, click.Command | None, list[str]]: + if args and args[0] not in self.commands and any(arg.startswith("-") for arg in args[1:]): + self._raise_legacy_order_error(ctx) + return super().resolve_command(ctx, args) + + def _raise_legacy_order_error(self, ctx: click.Context) -> None: + click.echo(ctx.get_help()) + click.echo( + "\nError: Invalid option order for `specfact code import`.\n" + "Use the canonical form: specfact code import --repo . \n" + "Or use the explicit command: specfact code import from-code --repo ." + ) + raise click.UsageError("Invalid option order for specfact code import") + + def _parse_explicit_subcommand_args(self, ctx: click.Context, args: list[str]) -> list[str]: + original_params = self.params + self.params = [param for param in self.params if not isinstance(param, click.Argument)] + try: + return super().parse_args(ctx, args) + finally: + self.params = original_params + + app = typer.Typer( help="Import codebases and related external inputs into SpecFact project bundles.", context_settings={"help_option_names": ["-h", "--help", "--help-advanced", "-ha"]}, invoke_without_command=True, no_args_is_help=False, + cls=_ImportCommandGroup, ) @@ -26,8 +74,8 @@ "Bundle name must be None or non-empty string", ) @require(lambda confidence: 0.0 <= confidence <= 1.0, "Confidence must be 0.0-1.0") -@beartype def import_codebase( + ctx: typer.Context, bundle: str | None = typer.Argument( None, help="Project bundle name (e.g., legacy-api, auth-module). Default: active plan from 'specfact plan select'.", @@ -99,6 +147,8 @@ def import_codebase( ), ) -> None: """Import a codebase into a SpecFact project bundle.""" + if ctx.invoked_subcommand is not None: + return legacy_from_code( bundle=bundle, repo=repo, @@ -115,7 +165,7 @@ def import_codebase( ) -app.command("from-code", hidden=True)(legacy_from_code) +app.command("from-code")(legacy_from_code) app.command("from-bridge")(legacy_from_bridge) diff --git a/packages/specfact-codebase/src/specfact_codebase/repro/commands.py b/packages/specfact-codebase/src/specfact_codebase/repro/commands.py index b46d3198..6f2b8b74 100644 --- a/packages/specfact-codebase/src/specfact_codebase/repro/commands.py +++ b/packages/specfact-codebase/src/specfact_codebase/repro/commands.py @@ -7,11 +7,11 @@ from __future__ import annotations +import importlib.util from pathlib import Path import typer from beartype import beartype -from click import Context as ClickContext from icontract import require from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn @@ -75,14 +75,9 @@ def _update_pyproject_crosshair_config(pyproject_path: Path, config: dict[str, i except ImportError: # Fallback: use tomllib/tomli to read, then append section manually - try: - import tomllib - except ImportError: - try: - import tomli as tomllib # noqa: F401 - except ImportError: - console.print("[red]Error:[/red] No TOML library available (need tomlkit, tomllib, or tomli)") - return False + if importlib.util.find_spec("tomllib") is None and importlib.util.find_spec("tomli") is None: + console.print("[red]Error:[/red] No TOML library available (need tomlkit, tomllib, or tomli)") + return False # Read existing content existing_content = "" @@ -135,7 +130,7 @@ def _count_python_files(path: Path) -> int: # CrossHair: Skip analysis for Typer-decorated functions (signature analysis limitation) # type: ignore[crosshair] def main( - ctx: ClickContext, + ctx: typer.Context, # Target/Input repo: Path = typer.Option( Path("."), diff --git a/packages/specfact-codebase/src/specfact_codebase/validate/commands.py b/packages/specfact-codebase/src/specfact_codebase/validate/commands.py index f9f6dc1c..82235387 100644 --- a/packages/specfact-codebase/src/specfact_codebase/validate/commands.py +++ b/packages/specfact-codebase/src/specfact_codebase/validate/commands.py @@ -143,11 +143,11 @@ def init( **Example:** ```bash - specfact validate sidecar init legacy-api /path/to/repo + specfact code validate sidecar init legacy-api /path/to/repo ``` **Next steps:** - After initialization, run `specfact validate sidecar run` to execute validation. + After initialization, run `specfact code validate sidecar run` to execute validation. """ if is_debug_mode(): debug_log_operation( @@ -211,13 +211,13 @@ def run( **Example:** ```bash # Run full validation (CrossHair + Specmatic) - specfact validate sidecar run legacy-api /path/to/repo + specfact code validate sidecar run legacy-api /path/to/repo # Run only CrossHair analysis - specfact validate sidecar run legacy-api /path/to/repo --no-run-specmatic + specfact code validate sidecar run legacy-api /path/to/repo --no-run-specmatic # Run only Specmatic validation - specfact validate sidecar run legacy-api /path/to/repo --no-run-crosshair + specfact code validate sidecar run legacy-api /path/to/repo --no-run-crosshair ``` **Output:** diff --git a/packages/specfact-govern/module-package.yaml b/packages/specfact-govern/module-package.yaml index 72d8573c..387b94f2 100644 --- a/packages/specfact-govern/module-package.yaml +++ b/packages/specfact-govern/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-govern -version: 0.40.21 +version: 0.40.23 commands: - govern tier: official @@ -19,5 +19,5 @@ description: Official SpecFact governance bundle package. category: govern bundle_group_command: govern integrity: - checksum: sha256:f56d74d4d87e77ad73b2978ac86adfeef969e74ac826b0ca7dd05d5fbf8c8fe5 - signature: omLeaJRZF6n+yq0CibNxgvp49i5AvzvGgIqtzRg5UzxpGaBV/PTjK4mPQi20YZfuD8DYzEr/AMxJDasH739QAQ== + checksum: sha256:b08213f3b3dfe4dee712646272a204b2f28cd57cf6145edd8d851abacd3635c5 + signature: HY3pzZz8ryiohpLlG0eVcQIda5/Ls6EMAOwDxQZNKbld8Zmg6kZD0pQtMQ86e2i774D963G6lZvaGs/QE3LYBA== diff --git a/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md b/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md index c393f272..38f85553 100644 --- a/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md @@ -111,13 +111,11 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: ## Available CLI Commands -- `specfact plan init ` - Initialize project bundle -- `specfact plan select ` - Set active plan (used as default for other commands) -- `specfact code import [] --repo ` - Import from codebase (uses active plan if bundle not specified) -- `specfact plan review []` - Review plan (uses active plan if bundle not specified) -- `specfact plan harden []` - Create SDD manifest (uses active plan if bundle not specified) -- `specfact govern enforce sdd []` - Validate SDD (uses active plan if bundle not specified) -- `specfact sync bridge --adapter --repo ` - Sync with external tools +- `specfact code import from-code --repo ` - Create or refresh a project bundle from code +- `specfact project health-check --repo --project-name ` - Verify project bundle health +- `specfact project export --repo --bundle --stdout` - Review project bundle content +- `specfact govern enforce sdd ` - Validate SDD for a project bundle +- `specfact project sync bridge --adapter --repo ` - Sync with external tools - See [Command Reference](../../docs/reference/commands.md) for full list -**Note**: Most commands now support active plan fallback. If `--bundle` is not specified, commands automatically use the active plan set via `plan select`. This improves workflow efficiency in AI IDE environments. +**Note**: Use explicit bundle or project-name arguments in prompts. If an option is unclear, inspect the current `specfact project --help` output and use the command-specific option shown there. diff --git a/packages/specfact-govern/resources/prompts/specfact.05-enforce.md b/packages/specfact-govern/resources/prompts/specfact.05-enforce.md index cce91745..647b5f97 100644 --- a/packages/specfact-govern/resources/prompts/specfact.05-enforce.md +++ b/packages/specfact-govern/resources/prompts/specfact.05-enforce.md @@ -28,7 +28,7 @@ Validate SDD manifest against project bundle and contracts. Checks hash matching ### Target/Input -- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan (set via `plan select`) +- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: explicit bundle name - `--sdd PATH` - Path to SDD manifest. Default: bundle-specific .specfact/projects//sdd. (Phase 8.5), with fallback to legacy .specfact/sdd/. ### Output/Results @@ -115,7 +115,7 @@ specfact govern enforce sdd [] [--sdd ] --no-interactive ```bash # Apply fixes via CLI commands, then re-validate -specfact plan update-feature [--bundle ] [options] --no-interactive +specfact code import from-code --repo . specfact govern enforce sdd [] --no-interactive ``` @@ -153,7 +153,7 @@ Issues Found: Hash changes when modifying features, stories, or product/idea/business sections. Note: Clarifications don't affect hash (review metadata). Hash stable across review sessions. - Fix: Run `specfact plan harden ` to update SDD manifest. + Fix: Run `specfact code import from-code --repo . ` to update SDD manifest. ``` ## Common Patterns diff --git a/packages/specfact-govern/src/specfact_govern/enforce/commands.py b/packages/specfact-govern/src/specfact_govern/enforce/commands.py index 1a87fea2..22ed1e72 100644 --- a/packages/specfact-govern/src/specfact_govern/enforce/commands.py +++ b/packages/specfact-govern/src/specfact_govern/enforce/commands.py @@ -119,9 +119,9 @@ def stage( - **Advanced/Configuration**: --preset **Examples:** - specfact enforce stage --preset balanced - specfact enforce stage --preset strict - specfact enforce stage --preset minimal + specfact govern enforce stage --preset balanced + specfact govern enforce stage --preset strict + specfact govern enforce stage --preset minimal """ if is_debug_mode(): debug_log_operation("command", "enforce stage", "started", extra={"preset": preset}) @@ -253,9 +253,9 @@ def enforce_sdd( - **Behavior/Options**: --no-interactive **Examples:** - specfact enforce sdd legacy-api - specfact enforce sdd auth-module --output-format json --out validation-report.json - specfact enforce sdd legacy-api --no-interactive + specfact govern enforce sdd legacy-api + specfact govern enforce sdd auth-module --output-format json --out validation-report.json + specfact govern enforce sdd legacy-api --no-interactive """ if is_debug_mode(): debug_log_operation( diff --git a/packages/specfact-project/module-package.yaml b/packages/specfact-project/module-package.yaml index 06b52b66..f740942b 100644 --- a/packages/specfact-project/module-package.yaml +++ b/packages/specfact-project/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-project -version: 0.41.18 +version: 0.41.23 commands: - project - plan @@ -27,5 +27,5 @@ core_compatibility: '>=0.40.0,<1.0.0' description: Official SpecFact project bundle package. bundle_group_command: project integrity: - checksum: sha256:4f2905d81e5287a7bfa81a93fe50ac17d6f55f5d5b7ea6b2d328ed83869d99a2 - signature: 55f2vT+jV/u0xcxWFgQnEZ1urtuJjfOhXYNo1tOUrnPZGhHWAhLaBGuwbYYb1V2FeUbFPdUxbbkx/TjbcLvZDA== + checksum: sha256:34cc434009a5e06cc2565584a0713cd418105d7961f7683ecb116eb73086bfab + signature: yBgNCMzBT6dNJOVEHajZlJneB/0ice84uaGYNkxUF6avTn+ZMmGVwWbf6HI4QEeXrohxLF/9PI1mafpqHro2Ag== diff --git a/packages/specfact-project/resources/prompts/shared/cli-enforcement.md b/packages/specfact-project/resources/prompts/shared/cli-enforcement.md index c393f272..38f85553 100644 --- a/packages/specfact-project/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-project/resources/prompts/shared/cli-enforcement.md @@ -111,13 +111,11 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: ## Available CLI Commands -- `specfact plan init ` - Initialize project bundle -- `specfact plan select ` - Set active plan (used as default for other commands) -- `specfact code import [] --repo ` - Import from codebase (uses active plan if bundle not specified) -- `specfact plan review []` - Review plan (uses active plan if bundle not specified) -- `specfact plan harden []` - Create SDD manifest (uses active plan if bundle not specified) -- `specfact govern enforce sdd []` - Validate SDD (uses active plan if bundle not specified) -- `specfact sync bridge --adapter --repo ` - Sync with external tools +- `specfact code import from-code --repo ` - Create or refresh a project bundle from code +- `specfact project health-check --repo --project-name ` - Verify project bundle health +- `specfact project export --repo --bundle --stdout` - Review project bundle content +- `specfact govern enforce sdd ` - Validate SDD for a project bundle +- `specfact project sync bridge --adapter --repo ` - Sync with external tools - See [Command Reference](../../docs/reference/commands.md) for full list -**Note**: Most commands now support active plan fallback. If `--bundle` is not specified, commands automatically use the active plan set via `plan select`. This improves workflow efficiency in AI IDE environments. +**Note**: Use explicit bundle or project-name arguments in prompts. If an option is unclear, inspect the current `specfact project --help` output and use the command-specific option shown there. diff --git a/packages/specfact-project/resources/prompts/specfact.02-plan.md b/packages/specfact-project/resources/prompts/specfact.02-plan.md index 3152deb6..7869d451 100644 --- a/packages/specfact-project/resources/prompts/specfact.02-plan.md +++ b/packages/specfact-project/resources/prompts/specfact.02-plan.md @@ -28,7 +28,7 @@ Manage project bundles: initialize, add features/stories, update metadata (idea/ ### Target/Input -- `--bundle NAME` - Project bundle name (optional, defaults to active plan set via `plan select`) +- `--bundle NAME` - Project bundle name (optional, defaults to explicit bundle name) - `--key KEY` - Feature/story key (e.g., FEATURE-001, STORY-001) - `--feature KEY` - Parent feature key (for story operations) @@ -60,12 +60,12 @@ Manage project bundles: initialize, add features/stories, update metadata (idea/ ### Step 2: Execute CLI ```bash -specfact plan init [--interactive/--no-interactive] [--scaffold/--no-scaffold] -specfact plan add-feature [--bundle ] --key --title [--outcomes <outcomes>] [--acceptance <acceptance>] -specfact plan add-story [--bundle <name>] --feature <feature-key> --key <story-key> --title <title> [--acceptance <acceptance>] -specfact plan update-idea [--bundle <name>] [--title <title>] [--narrative <narrative>] [--target-users <users>] [--value-hypothesis <hypothesis>] [--constraints <constraints>] -specfact plan update-feature [--bundle <name>] --key <key> [--title <title>] [--outcomes <outcomes>] [--acceptance <acceptance>] [--constraints <constraints>] [--confidence <score>] [--draft/--no-draft] -specfact plan update-story [--bundle <name>] --feature <feature-key> --key <story-key> [--title <title>] [--acceptance <acceptance>] [--story-points <points>] [--value-points <points>] [--confidence <score>] [--draft/--no-draft] +specfact code import from-code --repo . <bundle-name> +specfact code import from-code --repo . <bundle-name> +specfact code import from-code --repo . <bundle-name> +specfact code import from-code --repo . <bundle-name> +specfact code import from-code --repo . <bundle-name> +specfact code import from-code --repo . <bundle-name> # --bundle defaults to active plan if not specified ``` @@ -95,7 +95,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact plan <operation> [--bundle <name>] [options] +specfact code import from-code --repo . <bundle-name> ``` **Capture**: @@ -132,9 +132,9 @@ specfact plan <operation> [--bundle <name>] [options] ```bash # Use enrichment to update plan via CLI -specfact plan update-feature [--bundle <name>] --key <key> [options] +specfact code import from-code --repo . <bundle-name> # Or use batch updates: -specfact plan update-feature [--bundle <name>] --batch-updates <updates.json> +specfact code import from-code --repo . <bundle-name> ``` **Result**: Final artifacts are CLI-generated with validated enrichments @@ -161,8 +161,8 @@ Outcomes: Secure login, Session management ## Error (Missing Bundle) ```text -✗ Project bundle name is required (or set active plan with 'plan select') -Usage: specfact plan <operation> [--bundle <name>] [options] +✗ Project bundle name is required (or set explicit bundle name) +Usage: specfact code import from-code --repo . <bundle-name> ``` ## Common Patterns diff --git a/packages/specfact-project/resources/prompts/specfact.03-review.md b/packages/specfact-project/resources/prompts/specfact.03-review.md index 7808b9e8..a436f120 100644 --- a/packages/specfact-project/resources/prompts/specfact.03-review.md +++ b/packages/specfact-project/resources/prompts/specfact.03-review.md @@ -33,11 +33,11 @@ Act as a project review guide, not an artifact author. Use SpecFact CLI output a Before reviewing or answering questions, verify the current command surface when needed: ```bash -specfact plan review --help -specfact plan review [<bundle>] --list-questions --output-questions /tmp/specfact-review-questions.json +specfact project export --repo . --bundle <bundle-name> --stdout +specfact project export --repo . --bundle <bundle-name> --stdout ``` -If an option fails, inspect `specfact plan review --help`, choose the nearest safe non-interactive alternative, and ask the user when no clear mapping exists. Do not write `.specfact/` artifacts directly; route artifact updates back through SpecFact CLI commands or CLI-consumed enrichment/answers files. +If an option fails, inspect `specfact project --help`, choose the nearest safe non-interactive alternative, and ask the user when no clear mapping exists. Do not write `.specfact/` artifacts directly; route artifact updates back through SpecFact CLI commands or CLI-consumed enrichment/answers files. ## Interactive Question Presentation @@ -112,7 +112,7 @@ The recommendation helps less-experienced users make informed decisions. ### Target/Input -- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan (set via `plan select`) +- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: explicit bundle name - `--category CATEGORY` - Focus on specific taxonomy category. Default: None (all categories) ### Output/Results @@ -164,7 +164,7 @@ For these cases, use the **export-to-file → LLM reasoning → import-from-file ```bash # Export questions to file (REQUIRED for LLM enrichment workflow) # Use /tmp/ to avoid polluting the codebase -specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/questions.json +specfact project export --repo . --bundle <bundle-name> --stdout # Uses active plan if bundle not specified ``` @@ -174,10 +174,10 @@ specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/qu # Get findings (saves to stdout - can redirect to /tmp/) # Use /tmp/ to avoid polluting the codebase # Option 1: Redirect output (includes CLI banner - not recommended) -specfact plan review [<bundle-name>] --list-findings --findings-format json > /tmp/findings.json +specfact project export --repo . --bundle <bundle-name> --stdout # Option 2: Save directly to file (recommended - clean JSON only) -specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json +specfact project export --repo . --bundle <bundle-name> --stdout ``` **Note**: The `--output-questions` option saves questions directly to a file, avoiding the need for complex JSON parsing. The ambiguity scanner now recognizes the simplified format (e.g., "Must verify X works correctly (see contract examples)") as valid and will not flag it as vague. @@ -300,7 +300,7 @@ specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/find ```bash # Use /tmp/ to avoid polluting the codebase - specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/questions.json + /specfact.03-review <bundle-name> --list-questions --output-questions /tmp/questions.json ``` 2. **LLM reasoning and user selection** (Step 3): @@ -314,7 +314,7 @@ specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/find ```bash # Import answers from exported file # Use /tmp/ to avoid polluting the codebase - specfact plan review [<bundle-name>] --answers /tmp/answers.json + /specfact.03-review <bundle-name> --answers /tmp/answers.json ``` **CRITICAL**: @@ -331,13 +331,13 @@ specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/find Use `plan update-idea` to update idea fields from enrichment recommendations: ```bash -specfact plan update-idea --bundle [<bundle-name>] --value-hypothesis "..." --narrative "..." --target-users "..." +specfact project export --repo . --bundle <bundle-name> --stdout ``` #### Option C: Apply enrichment via import (only if bundle needs regeneration) ```bash -specfact code import [<bundle-name>] --repo . --enrichment enrichment-report.md +specfact code import from-code --repo . <bundle-name> --enrichment enrichment-report.md ``` **Note:** @@ -390,10 +390,10 @@ When in copilot mode, follow this three-phase workflow: ```bash # Option 1: Get findings (redirect to /tmp/ to avoid polluting codebase) # Option 1: Save findings directly to file (recommended - clean JSON only) -specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json +specfact project export --repo . --bundle <bundle-name> --stdout # Option 2: Get questions and save directly to /tmp/ (recommended - avoids JSON parsing) -specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/questions.json +specfact project export --repo . --bundle <bundle-name> --stdout ``` **Capture**: @@ -422,7 +422,7 @@ specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/qu 0. **Grounding rule**: - Treat CLI-exported questions as the source of truth; consult codebase/docs only to answer them (do not invent new artifacts) - **Feature/Story Completeness note**: Answers here are clarifications only. They do **NOT** create stories. - For missing stories, use `specfact plan add-story` (or `plan update-story --batch-updates` if stories already exist). + For missing stories, use `specfact project export --repo . --bundle <bundle-name> --stdout`. 1. **Read exported questions file** (`/tmp/questions.json`): - Review all questions and their categories @@ -495,17 +495,17 @@ specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/qu ```bash # Import answers from /tmp/answers.json file # Use /tmp/ to avoid polluting the codebase -specfact plan review [<bundle-name>] --answers /tmp/answers.json +specfact project export --repo . --bundle <bundle-name> --stdout ``` **For non-partial findings only:** ```bash # Use auto-enrich for simple vague criteria (not partial findings) -specfact plan review [<bundle-name>] --auto-enrich +specfact project export --repo . --bundle <bundle-name> --stdout # Or use batch updates for feature updates -specfact plan update-feature [--bundle <name>] --batch-updates <updates.json> +specfact project export --repo . --bundle <bundle-name> --stdout ``` **Result**: Final artifacts are CLI-generated with validated enrichments @@ -537,7 +537,7 @@ Coverage Summary: ```text ✗ Project bundle 'legacy-api' not found -Create one with: specfact plan init legacy-api +Create one with: specfact code import from-code --repo . <bundle-name> ``` ## Common Patterns @@ -585,13 +585,13 @@ Create one with: specfact plan init legacy-api 1. **Export questions to file** (use `/tmp/` to avoid polluting codebase): ```bash - specfact plan review [<bundle-name>] --list-questions --output-questions /tmp/questions.json + /specfact.03-review <bundle-name> --list-questions --output-questions /tmp/questions.json ``` 2. **Get findings** (optional, for comprehensive analysis - use `/tmp/`): ```bash - specfact plan review [<bundle-name>] --list-findings --output-findings /tmp/findings.json + /specfact.03-review <bundle-name> --list-findings --output-findings /tmp/findings.json ``` 3. **LLM reasoning and user selection** (REQUIRED for partial findings): @@ -612,7 +612,7 @@ Create one with: specfact plan init legacy-api ```bash # Import answers from exported file - specfact plan review [<bundle-name>] --answers /tmp/answers.json + /specfact.03-review <bundle-name> --answers /tmp/answers.json ``` **CRITICAL**: Use the file path `/tmp/answers.json` (not a JSON string extracted from `/tmp/questions.json`) @@ -624,7 +624,7 @@ Create one with: specfact plan init legacy-api **For non-partial findings only:** - **During import**: Auto-enrichment happens automatically (enabled by default) -- **After import**: Use `specfact plan review --auto-enrich` for simple vague criteria +- **After import**: Use `specfact project export --repo . --bundle <bundle-name> --stdout` to inspect simple vague criteria - **Note**: The scanner now recognizes simplified format (e.g., "Must verify X works correctly (see contract examples)") as valid **Alternative approaches** (for business context only): @@ -733,8 +733,8 @@ When generating enrichment reports for use with `import from-code --enrichment`, Use CLI output as verification evidence: ```bash -specfact plan review [<bundle>] --list-findings --output-findings /tmp/specfact-review-findings.json -specfact plan review [<bundle>] --list-questions --output-questions /tmp/specfact-review-questions.json +specfact project export --repo . --bundle <bundle-name> --stdout +specfact project export --repo . --bundle <bundle-name> --stdout ``` For module development in this repository, validate packaged prompt and module payload changes with: diff --git a/packages/specfact-project/resources/prompts/specfact.04-sdd.md b/packages/specfact-project/resources/prompts/specfact.04-sdd.md index 6dc53853..63c747ea 100644 --- a/packages/specfact-project/resources/prompts/specfact.04-sdd.md +++ b/packages/specfact-project/resources/prompts/specfact.04-sdd.md @@ -28,7 +28,7 @@ Create/update SDD manifest from project bundle. Captures WHY (intent/constraints ### Target/Input -- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan (set via `plan select`) +- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan - `--sdd PATH` - Output SDD manifest path. Default: bundle-specific .specfact/projects/<bundle-name>/sdd.<format> (Phase 8.5) ### Output/Results @@ -49,7 +49,7 @@ Create/update SDD manifest from project bundle. Captures WHY (intent/constraints ### Step 2: Execute CLI ```bash -specfact plan harden [<bundle-name>] [--sdd <path>] [--output-format <format>] +specfact govern enforce sdd <bundle-name> # Uses active plan if bundle not specified ``` @@ -78,7 +78,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact plan harden [<bundle-name>] [--sdd <path>] --no-interactive +specfact govern enforce sdd <bundle-name> ``` **Capture**: @@ -110,9 +110,8 @@ specfact plan harden [<bundle-name>] [--sdd <path>] --no-interactive ### Phase 3: CLI Artifact Creation (REQUIRED) ```bash -# Use enrichment to update plan via CLI, then regenerate SDD -specfact plan update-idea [--bundle <name>] [options] --no-interactive -specfact plan harden [<bundle-name>] --no-interactive +# Regenerate SDD from the updated plan bundle +specfact govern enforce sdd <bundle-name> ``` **Result**: Final SDD is CLI-generated with validated enrichments @@ -147,7 +146,7 @@ Contracts: 15 ```text ✗ Project bundle 'legacy-api' not found -Create one with: specfact plan init legacy-api +Create one with: specfact code import from-code --repo . <bundle-name> ``` ## Common Patterns diff --git a/packages/specfact-project/resources/prompts/specfact.06-sync.md b/packages/specfact-project/resources/prompts/specfact.06-sync.md index 79aaef1b..f5506531 100644 --- a/packages/specfact-project/resources/prompts/specfact.06-sync.md +++ b/packages/specfact-project/resources/prompts/specfact.06-sync.md @@ -70,13 +70,13 @@ Synchronize artifacts from external tools (Spec-Kit, Linear, Jira) with SpecFact ```bash # Spec-Kit adapter (default) -specfact sync bridge --adapter speckit --repo <path> [--bidirectional] [--bundle <name>] [--overwrite] [--watch] [--interval <seconds>] +specfact project sync bridge --adapter speckit --repo <path> [--bidirectional] [--bundle <name>] [--overwrite] [--watch] [--interval <seconds>] # GitHub adapter (for backlog sync) -specfact sync bridge --adapter github --repo <path> --repo-owner <owner> --repo-name <name> [--bidirectional] [--bundle <name>] [--github-token <token>] [--use-gh-cli] +specfact project sync bridge --adapter github --repo <path> --repo-owner <owner> --repo-name <name> [--bidirectional] [--bundle <name>] [--github-token <token>] [--use-gh-cli] # Azure DevOps adapter (for backlog sync) -specfact sync bridge --adapter ado --repo <path> --ado-org <org> --ado-project <project> [--bidirectional] [--bundle <name>] [--ado-token <token>] [--ado-base-url <url>] +specfact project sync bridge --adapter ado --repo <path> --ado-org <org> --ado-project <project> [--bidirectional] [--bundle <name>] [--ado-token <token>] [--ado-base-url <url>] # --bundle defaults to active plan if not specified ``` @@ -108,7 +108,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact sync bridge --adapter <adapter> --repo <path> [options] +specfact project sync bridge --adapter <adapter> --repo <path> [options] ``` **Capture**: @@ -141,8 +141,8 @@ specfact sync bridge --adapter <adapter> --repo <path> [options] ```bash # Apply resolutions via CLI commands, then re-sync -specfact plan update-feature [--bundle <name>] [options] -specfact sync bridge --adapter <adapter> --repo <path> +specfact project sync bridge --adapter speckit --repo . --bundle <bundle-name> +specfact project sync bridge --adapter <adapter> --repo <path> ``` **Result**: Final artifacts are CLI-generated with validated resolutions @@ -177,12 +177,12 @@ Supported adapters: speckit, generic-markdown, openspec, github, ado ```text ✗ GitHub adapter requires both --repo-owner and --repo-name options -Example: specfact sync bridge --adapter github --repo-owner 'nold-ai' --repo-name 'specfact-cli' --bidirectional +Example: specfact project sync bridge --adapter github --repo-owner 'nold-ai' --repo-name 'specfact-cli' --bidirectional ``` ```text ✗ Azure DevOps adapter requires both --ado-org and --ado-project options -Example: specfact sync bridge --adapter ado --ado-org 'my-org' --ado-project 'my-project' --bidirectional +Example: specfact project sync bridge --adapter ado --ado-org 'my-org' --ado-project 'my-project' --bidirectional ``` ## Common Patterns diff --git a/packages/specfact-project/resources/prompts/specfact.08-simplify.md b/packages/specfact-project/resources/prompts/specfact.08-simplify.md index 25477196..77e35114 100644 --- a/packages/specfact-project/resources/prompts/specfact.08-simplify.md +++ b/packages/specfact-project/resources/prompts/specfact.08-simplify.md @@ -44,10 +44,10 @@ Before reading or editing source, verify the current command surface when needed ```bash specfact code review run --help specfact code review run --instructions -specfact code review run --scope changed --focus simplify --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --json --out .specfact/code-review-simplify.json ``` -If this slash prompt or the installed skill is unavailable in another AI IDE, tell the user they can run `specfact code review run --instructions` and paste that output to the AI assistant. If `--focus simplify` is unavailable in the installed CLI, self-heal by inspecting `specfact code review run --help`, then run the closest non-destructive JSON review command that preserves advisory findings, usually without `--level error`. +If this slash prompt or the installed skill is unavailable in another AI IDE, tell the user they can run `specfact code review run --instructions` and paste that output to the AI assistant. If `--focus simplify` or `--enforcement shadow` is unavailable in the installed CLI, self-heal by inspecting `specfact code review run --help`, then run the closest non-destructive JSON review command that preserves advisory findings, usually without `--level error`. ## Workflow @@ -56,7 +56,7 @@ If this slash prompt or the installed skill is unavailable in another AI IDE, te Read `.specfact/code-review-simplify.json`. If it is missing, ask the user to run: ```bash -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json ``` Explain that this report is the evidence file: it lists candidate cleanups, cleanup forecast, safety checks, remediation packets, and preserve reasons the assistant must use before touching code. Do not edit files until the report exists. @@ -114,7 +114,7 @@ Use the evidence column for removed findings, required tests, skipped safety che After accepted edits are applied, suggest: ```bash -specfact code review run --scope changed --focus simplify --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement shadow --focus simplify --json --out .specfact/code-review-simplify.json ``` Compare the new report with the prior findings and summarize which `ai_bloat` or metadata-backed simplification candidates were recommended, applied, kept, skipped, failed, cleared, or still present. Include evidence of improvement such as removed findings, estimated deletion lines, simpler control flow, or preserved contracts. @@ -124,8 +124,8 @@ Compare the new report with the prior findings and summarize which `ai_bloat` or Use the CLI as the verification source: ```bash -specfact code review run --scope changed --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json -specfact code review run --scope changed --bug-hunt --json --out .specfact/code-review-bughunt.json +specfact code review run --scope changed --enforcement shadow --focus simplify --preview-fixes --json --out .specfact/code-review-simplify.json +specfact code review run --scope changed --enforcement changed --bug-hunt --json --out .specfact/code-review-bughunt.json ``` For module development in this repository, the expected local gates are: diff --git a/packages/specfact-project/resources/prompts/specfact.compare.md b/packages/specfact-project/resources/prompts/specfact.compare.md index 170e6a61..cf09645a 100644 --- a/packages/specfact-project/resources/prompts/specfact.compare.md +++ b/packages/specfact-project/resources/prompts/specfact.compare.md @@ -51,8 +51,8 @@ Compare two project bundles (or legacy plan bundles) to detect deviations, misma ### Step 2: Execute CLI ```bash -specfact plan compare [--bundle <bundle-name>] [--manual <path>] [--auto <path>] [--code-vs-plan] [--output-format <format>] [--out <path>] -# --bundle defaults to active plan if not specified +specfact project health-check --repo . --project-name <bundle-name> +specfact project export --repo . --bundle <bundle-name> --stdout ``` ### Step 3: Present Results @@ -81,15 +81,16 @@ When in copilot mode, follow this three-phase workflow: ### Phase 1: CLI Grounding (REQUIRED) ```bash -# Execute CLI to get structured output -specfact plan compare [--bundle <name>] [options] +# Execute CLI to get structured project state +specfact project health-check --repo . --project-name <bundle-name> +specfact project export --repo . --bundle <bundle-name> --stdout ``` **Capture**: -- CLI-generated comparison report -- Deviation counts and severity -- Missing features analysis +- CLI-generated project health status +- Exported project bundle content +- Any missing or invalid project metadata ### Phase 2: LLM Enrichment (OPTIONAL, Copilot Only) @@ -114,9 +115,9 @@ specfact plan compare [--bundle <name>] [options] ### Phase 3: CLI Artifact Creation (REQUIRED) ```bash -# Apply fixes via CLI commands, then re-compare -specfact plan update-feature [--bundle <name>] [options] -specfact plan compare [--bundle <name>] +# Apply fixes via CLI commands, then validate current project state +specfact code import from-code --repo . <bundle-name> +specfact project health-check --repo . --project-name <bundle-name> ``` **Result**: Final artifacts are CLI-generated with validated fixes @@ -146,7 +147,7 @@ Missing in Auto Plan: 1 feature ```text ✗ Default manual plan not found: .specfact/plans/main.bundle.yaml -Create one with: specfact plan init --interactive +Create one with: specfact code import from-code --repo . <bundle-name> ``` ## Common Patterns diff --git a/packages/specfact-project/resources/templates/github-action.yml.j2 b/packages/specfact-project/resources/templates/github-action.yml.j2 index 0096049b..c48cbf46 100644 --- a/packages/specfact-project/resources/templates/github-action.yml.j2 +++ b/packages/specfact-project/resources/templates/github-action.yml.j2 @@ -24,4 +24,8 @@ jobs: run: pip install specfact-cli - name: Run validation - run: specfact validate --repo . --name "{{ repo_name }}" + run: | + # Operating guidance only, not the source of truth. CLI help is authoritative. + # If this command drifts, inspect `specfact project health-check --help` + # and ask the user if no safe correction is clear. + specfact project health-check --repo . --project-name "{{ repo_name }}" diff --git a/packages/specfact-project/src/specfact_project/analyzers/code_analyzer.py b/packages/specfact-project/src/specfact_project/analyzers/code_analyzer.py index ab1763ab..b0ae3c1e 100644 --- a/packages/specfact-project/src/specfact_project/analyzers/code_analyzer.py +++ b/packages/specfact-project/src/specfact_project/analyzers/code_analyzer.py @@ -6,7 +6,6 @@ import json import os import re -import shutil import subprocess from collections import defaultdict from concurrent.futures import ThreadPoolExecutor, as_completed @@ -391,26 +390,22 @@ def analyze_file_safe(file_path: Path) -> dict[str, Any]: clarifications=None, ) - def _check_semgrep_available(self) -> bool: - """Check if Semgrep is available in PATH.""" + def _probe_semgrep(self) -> tuple[bool, str | None]: + """Check if Semgrep is available in the active repository environment.""" # Skip Semgrep check in test mode to avoid timeouts if os.environ.get("TEST_MODE") == "true": - return False + return False, "Semgrep skipped in TEST_MODE" - # Fast check: use shutil.which first to avoid subprocess overhead - if shutil.which("semgrep") is None: - return False + from specfact_cli.utils.env_manager import check_tool_in_env, detect_env_manager - try: - result = subprocess.run( - ["semgrep", "--version"], - capture_output=True, - text=True, - timeout=5, # Increased timeout to 5s (Semgrep may need time to initialize) - ) - return result.returncode == 0 - except (FileNotFoundError, subprocess.TimeoutExpired, OSError): - return False + env_info = detect_env_manager(self.repo_path) + available, message = check_tool_in_env(self.repo_path, "semgrep", env_info) + return available, message + + def _check_semgrep_available(self) -> bool: + """Check if Semgrep is available in the active repository environment.""" + available, _message = self._probe_semgrep() + return available def get_plugin_status(self) -> list[dict[str, Any]]: """ @@ -434,12 +429,12 @@ def get_plugin_status(self) -> list[dict[str, Any]]: ) # Semgrep Pattern Detection - semgrep_available = self._check_semgrep_available() + semgrep_available, semgrep_message = self._probe_semgrep() semgrep_enabled = self.semgrep_enabled and semgrep_available semgrep_used = semgrep_enabled and self.semgrep_config is not None if not semgrep_available: - reason = "Semgrep CLI not installed (install: pip install semgrep)" + reason = semgrep_message or "Semgrep CLI not installed (install: pip install semgrep)" elif self.semgrep_config is None: reason = "Semgrep config not found" else: @@ -497,8 +492,11 @@ def _run_semgrep_patterns(self, file_path: Path) -> list[dict[str, Any]]: return [] try: - # Check if semgrep is available quickly - if not shutil.which("semgrep"): + from specfact_cli.utils.env_manager import build_tool_command, check_tool_in_env, detect_env_manager + + env_info = detect_env_manager(self.repo_path) + semgrep_available, _message = check_tool_in_env(self.repo_path, "semgrep", env_info) + if not semgrep_available: return [] # Run feature detection @@ -511,7 +509,7 @@ def _run_semgrep_patterns(self, file_path: Path) -> list[dict[str, Any]]: timeout = 10 result = subprocess.run( - ["semgrep", "--config", *configs, "--json", str(file_path)], + build_tool_command(env_info, ["semgrep", "--config", *configs, "--json", str(file_path)]), capture_output=True, text=True, timeout=timeout, diff --git a/packages/specfact-project/src/specfact_project/import_cmd/commands.py b/packages/specfact-project/src/specfact_project/import_cmd/commands.py index 10e23f39..f40e5d3e 100644 --- a/packages/specfact-project/src/specfact_project/import_cmd/commands.py +++ b/packages/specfact-project/src/specfact_project/import_cmd/commands.py @@ -1670,7 +1670,7 @@ def _suggest_next_steps(repo: Path, bundle: str, plan_bundle: PlanBundle | None) console.print(" [dim]Detect deviations between plan and code[/dim]\n") console.print(" [yellow]→[/yellow] [bold]Validate SDD:[/bold]") - console.print(f" specfact enforce sdd {bundle}") + console.print(f" specfact govern enforce sdd {bundle}") console.print(" [dim]Check for violations and coverage thresholds[/dim]\n") else: console.print(" [yellow]→[/yellow] [bold]Review changes:[/bold]") @@ -1718,7 +1718,7 @@ def _suggest_constitution_bootstrap(repo: Path) -> None: console.print("[bold green]✓[/bold green] Bootstrap constitution generated") console.print(f"[dim]Review and adjust: {constitution_path}[/dim]") console.print( - "[dim]Then run 'specfact sync bridge --adapter <tool>' to sync with external tool artifacts[/dim]" + "[dim]Then run 'specfact project sync bridge --adapter <tool>' to sync with external tool artifacts[/dim]" ) else: console.print() @@ -1949,7 +1949,7 @@ def from_bridge( - openspec: OpenSpec integration (openspec/) - read-only sync (Phase 1) - generic-markdown: Generic markdown-based specifications - import & sync - Note: For backlog synchronization (GitHub Issues, ADO, Linear, Jira), use 'specfact sync bridge' instead. + Note: For backlog synchronization (GitHub Issues, ADO, Linear, Jira), use 'specfact project sync bridge' instead. **Parameter Groups:** - **Target/Input**: --repo @@ -2023,7 +2023,7 @@ def from_bridge( if not adapter_instance.detect(repo, bridge_config): console.print(f"[bold red]✗[/bold red] Not a {adapter_lower} repository") console.print(f"[dim]Expected: {adapter_lower} structure[/dim]") - console.print("[dim]Tip: Use 'specfact sync bridge probe' to auto-detect tool configuration[/dim]") + console.print("[dim]Tip: Use 'specfact project sync bridge probe' to auto-detect tool configuration[/dim]") raise typer.Exit(1) console.print(f"[bold green]✓[/bold green] Detected {adapter_lower} repository") @@ -2048,7 +2048,7 @@ def from_bridge( f"[bold yellow]⚠[/bold yellow] '{adapter_lower}' is a backlog adapter, not a code/spec adapter" ) console.print( - f"[dim]Use 'specfact sync bridge --adapter {adapter_lower}' for backlog synchronization[/dim]" + f"[dim]Use 'specfact project sync bridge --adapter {adapter_lower}' for backlog synchronization[/dim]" ) console.print( "[dim]The 'import from-bridge' command is for importing code/spec projects (Spec-Kit, OpenSpec, generic-markdown)[/dim]" @@ -2090,7 +2090,9 @@ def from_bridge( if not features: console.print(f"[bold red]✗[/bold red] No features found in {adapter_lower} repository") console.print("[dim]Expected: specs/*/spec.md files (or bridge-configured paths)[/dim]") - console.print("[dim]Tip: Use 'specfact sync bridge probe' to validate bridge configuration[/dim]") + console.print( + "[dim]Tip: Use 'specfact project sync bridge probe' to validate bridge configuration[/dim]" + ) raise typer.Exit(1) progress.update(task, description=f"✓ Discovered {len(features)} features") @@ -2489,12 +2491,12 @@ def from_code( - **Advanced/Configuration**: --confidence, --key-format **Examples:** - specfact code import legacy-api --repo . - specfact code import auth-module --repo . --enrichment enrichment-report.md - specfact code import my-project --repo . --confidence 0.7 --shadow-only - specfact code import my-project --repo . --force # Force full regeneration - specfact code import my-project --repo . # Test files excluded by default - specfact code import my-project --repo . --include-tests # Include test files in dependency graph + specfact code import --repo . legacy-api + specfact code import --repo . auth-module --enrichment enrichment-report.md + specfact code import --repo . my-project --confidence 0.7 --shadow-only + specfact code import --repo . my-project --force # Force full regeneration + specfact code import --repo . my-project # Test files excluded by default + specfact code import --repo . my-project --include-tests # Include test files in dependency graph """ from specfact_cli.cli import get_current_mode from specfact_cli.modes import get_router diff --git a/packages/specfact-project/src/specfact_project/plan/commands.py b/packages/specfact-project/src/specfact_project/plan/commands.py index b74e61d5..481a0430 100644 --- a/packages/specfact-project/src/specfact_project/plan/commands.py +++ b/packages/specfact-project/src/specfact_project/plan/commands.py @@ -2525,7 +2525,7 @@ def sync( """ Sync shared plans between Spec-Kit and SpecFact (bidirectional sync). - This is a convenience wrapper around `specfact sync bridge --adapter speckit --bidirectional` + This is a convenience wrapper around `specfact project sync bridge --adapter speckit --bidirectional` that enables team collaboration through shared structured plans. The bidirectional sync keeps Spec-Kit artifacts and SpecFact plans synchronized automatically. @@ -2555,7 +2555,7 @@ def sync( if not shared: print_error("This command requires --shared flag") print_info("Use 'specfact plan sync --shared' to enable shared plans sync") - print_info("Or use 'specfact sync bridge --adapter speckit --bidirectional' for direct sync") + print_info("Or use 'specfact project sync bridge --adapter speckit --bidirectional' for direct sync") raise typer.Exit(1) # Use default repo if not specified @@ -2733,7 +2733,7 @@ def promote( print_warning( f"SDD has {sdd_report.medium_count} medium and {sdd_report.low_count} low severity deviation(s)" ) - console.print("[dim]Run 'specfact enforce sdd' for detailed report[/dim]") + console.print("[dim]Run 'specfact govern enforce sdd' for detailed report[/dim]") if not force and not prompt_confirm( "Continue with promotion despite coverage threshold warnings?", default=False ): @@ -3609,7 +3609,7 @@ def _prepare_review_bundle( console.print(f" [bold yellow]⚠[/bold yellow] {deviation.description}") else: console.print(f" [dim]ℹ[/dim] {deviation.description}") - console.print("\n[dim]Run 'specfact enforce sdd' for detailed validation report[/dim]") + console.print("\n[dim]Run 'specfact govern enforce sdd' for detailed validation report[/dim]") else: print_success("SDD manifest validated successfully") @@ -3634,7 +3634,7 @@ def _prepare_review_bundle( if sdd_report.total_deviations > 0: console.print(f"\n[dim]Found {sdd_report.total_deviations} coverage threshold warning(s)[/dim]") - console.print("[dim]Run 'specfact enforce sdd' for detailed report[/dim]") + console.print("[dim]Run 'specfact govern enforce sdd' for detailed report[/dim]") # Initialize clarifications if needed from specfact_cli.models.plan import Clarifications diff --git a/packages/specfact-project/src/specfact_project/project/commands.py b/packages/specfact-project/src/specfact_project/project/commands.py index c6b10446..2dcb68c1 100644 --- a/packages/specfact-project/src/specfact_project/project/commands.py +++ b/packages/specfact-project/src/specfact_project/project/commands.py @@ -3,6 +3,10 @@ This module provides commands for persona-based editing, lock enforcement, and merge conflict resolution for project bundles. + +Operating guidance: command examples in this source are not the source of +truth; CLI help is authoritative. Check `specfact --help` or command-specific +`--help`, and ask the user before guessing when help output disagrees. """ from __future__ import annotations @@ -61,13 +65,13 @@ def _refresh_console() -> Console: @app.callback() -def _project_callback() -> None: +def _project_callback() -> None: # pyright: ignore[reportUnusedFunction] # Typer registers callbacks by decorator. """Ensure project command group always uses a fresh console for current process streams.""" _refresh_console() @version_app.callback() -def _project_version_callback() -> None: +def _project_version_callback() -> None: # pyright: ignore[reportUnusedFunction] # Typer registers callbacks by decorator. """Ensure project version subcommands also refresh console when invoked directly.""" _refresh_console() @@ -213,7 +217,7 @@ def _collect_backlog_health_metrics(adapter: str, project_id: str, template: str Use 'specfact backlog analyze-deps' instead. """ print_warning("Backlog health metrics functionality moved to 'backlog' command group.") - print_info("Use: specfact backlog analyze-deps") + print_info("Use: `specfact backlog analyze-deps`") return {"coverage": 0.0, "note": "Use 'specfact backlog analyze-deps' for backlog health metrics"} @@ -248,7 +252,7 @@ def _run_spec_code_alignment_check( args.append("--no-interactive") exit_code = subcommand.main( args=args, - prog_name="specfact enforce sdd", + prog_name="specfact govern enforce sdd", standalone_mode=False, ) if exit_code and exit_code != 0: @@ -280,7 +284,7 @@ def _run_release_readiness_check( Use 'specfact backlog verify-readiness' instead. """ print_warning("Release readiness check moved to 'backlog' command group.") - print_info("Use: specfact backlog verify-readiness") + print_info("Use: `specfact backlog verify-readiness`") return {"ok": True, "summary": "Use 'specfact backlog verify-readiness' for detailed checks"} @@ -311,20 +315,29 @@ def _fetch_backlog_graph(*, adapter: str, project_id: str, template: str) -> Any NOTE: This functionality has been moved to the 'backlog' command group. """ print_warning("Backlog graph functionality moved to 'backlog' command group.") - print_info("Use: specfact backlog analyze-deps or specfact backlog graph-export") + print_info("Use: `specfact backlog analyze-deps`") return None +def _require_backlog_graph(graph: Any, *, command_name: str) -> Any: + """Fail with a typed diagnostic when backlog graph data is unavailable.""" + if graph is None or not hasattr(graph, "items"): + print_error(f"Backlog graph data unavailable for specfact project {command_name}.") + print_info("Run `specfact backlog analyze-deps` to inspect backlog data.") + raise typer.Exit(1) + return graph + + @beartype @ensure(lambda result: isinstance(result, list), "Roadmap must be list") def generate_roadmap(*, adapter: str, project_id: str, template: str) -> list[str]: """Generate roadmap milestones from dependency critical path. NOTE: This functionality has been moved to the 'backlog' command group. - Use 'specfact backlog analyze-deps --critical-path' for roadmap generation. + Use 'specfact project export-roadmap' for roadmap generation. """ print_warning("Roadmap generation moved to 'backlog' command group.") - print_info("Use: specfact backlog analyze-deps --critical-path") + print_info("Use: `specfact project export-roadmap`") return [] @@ -537,7 +550,7 @@ def devops_flow( if normalized_stage == "develop" and normalized_action == "sync": print_warning("Backlog sync functionality moved to 'backlog' command group.") - print_info("Use: specfact backlog sync") + print_info("Use: `specfact backlog sync`") print_success("Develop/sync placeholder completed.") return @@ -561,7 +574,7 @@ def devops_flow( release_target = extract_release_target(bundle_obj) print_info(f"Release target: {release_target}") print_warning("Release notes generation moved to 'backlog' command group.") - print_info("Use: specfact backlog generate-release-notes") + print_info("Use: `specfact backlog verify-readiness`") print_success(f"Release verification passed for target '{release_target}'.") return @@ -595,6 +608,7 @@ def snapshot( bundle_obj = _load_bundle_with_progress(bundle_dir, validate_hashes=False) adapter, project_id, template = _resolve_linked_backlog_config(bundle_obj) graph = _fetch_backlog_graph(adapter=adapter, project_id=project_id, template=template) + graph = _require_backlog_graph(graph, command_name="snapshot") output.parent.mkdir(parents=True, exist_ok=True) output.write_text(graph.to_json(), encoding="utf-8") print_success(f"Snapshot written for bundle '{bundle_name}': {output}") @@ -634,6 +648,7 @@ def regenerate( bundle_obj = _load_bundle_with_progress(bundle_dir, validate_hashes=False) adapter, project_id, template = _resolve_linked_backlog_config(bundle_obj) graph = _fetch_backlog_graph(adapter=adapter, project_id=project_id, template=template) + graph = _require_backlog_graph(graph, command_name="regenerate") plan_view = {"items": [str(feature_key) for feature_key in bundle_obj.features if str(feature_key)]} backlog_view = {"items": [str(item_id) for item_id in graph.items]} diff --git a/packages/specfact-project/src/specfact_project/sync/commands.py b/packages/specfact-project/src/specfact_project/sync/commands.py index b233096c..d8b08c33 100644 --- a/packages/specfact-project/src/specfact_project/sync/commands.py +++ b/packages/specfact-project/src/specfact_project/sync/commands.py @@ -172,7 +172,7 @@ def sync_spec_kit( """ Compatibility helper for callers that previously imported `sync_spec_kit`. - Delegates to `sync bridge --adapter speckit` with concrete Python defaults, + Delegates to `project sync bridge --adapter speckit` with concrete Python defaults, avoiding direct invocation of Typer `OptionInfo` defaults. """ bundle = _extract_bundle_name_from_plan_path(plan) if plan is not None else None @@ -570,37 +570,37 @@ def sync_bridge( **Basic Examples:** - specfact sync bridge --adapter speckit --repo . --bidirectional - specfact sync bridge --adapter openspec --repo . --mode read-only # OpenSpec → SpecFact (read-only) - specfact sync bridge --adapter openspec --repo . --external-base-path ../other-repo # Cross-repo OpenSpec - specfact sync bridge --repo . --bidirectional # Auto-detect adapter - specfact sync bridge --repo . --watch --interval 10 + specfact project sync bridge --adapter speckit --repo . --bidirectional + specfact project sync bridge --adapter openspec --repo . --mode read-only # OpenSpec → SpecFact (read-only) + specfact project sync bridge --adapter openspec --repo . --external-base-path ../other-repo # Cross-repo OpenSpec + specfact project sync bridge --repo . --bidirectional # Auto-detect adapter + specfact project sync bridge --repo . --watch --interval 10 **GitHub Examples:** - specfact sync bridge --adapter github --bidirectional --repo-owner owner --repo-name repo # Bidirectional sync - specfact sync bridge --adapter github --mode export-only --repo-owner owner --repo-name repo # Export only - specfact sync bridge --adapter github --update-existing # Update existing issues when content changes - specfact sync bridge --adapter github --track-code-changes # Detect code changes and add progress comments - specfact sync bridge --adapter github --add-progress-comment # Add manual progress comment + specfact project sync bridge --adapter github --bidirectional --repo-owner owner --repo-name repo # Bidirectional sync + specfact project sync bridge --adapter github --mode export-only --repo-owner owner --repo-name repo # Export only + specfact project sync bridge --adapter github --update-existing # Update existing issues when content changes + specfact project sync bridge --adapter github --track-code-changes # Detect code changes and add progress comments + specfact project sync bridge --adapter github --add-progress-comment # Add manual progress comment **Azure DevOps Examples:** - specfact sync bridge --adapter ado --bidirectional --ado-org myorg --ado-project myproject # Bidirectional sync - specfact sync bridge --adapter ado --mode export-only --ado-org myorg --ado-project myproject # Export only - specfact sync bridge --adapter ado --mode export-only --ado-org myorg --ado-project myproject --bundle main # Bundle export + specfact project sync bridge --adapter ado --bidirectional --ado-org myorg --ado-project myproject # Bidirectional sync + specfact project sync bridge --adapter ado --mode export-only --ado-org myorg --ado-project myproject # Export only + specfact project sync bridge --adapter ado --mode export-only --ado-org myorg --ado-project myproject --bundle main # Bundle export **Cross-Adapter Sync Examples:** # GitHub → ADO Migration (lossless round-trip) - specfact sync bridge --adapter github --mode bidirectional --bundle migration --backlog-ids 123 + specfact project sync bridge --adapter github --mode bidirectional --bundle migration --backlog-ids 123 # Output shows: "✓ Imported GitHub issue #123 as change proposal: add-feature-x" - specfact sync bridge --adapter ado --mode export-only --bundle migration --change-ids add-feature-x + specfact project sync bridge --adapter ado --mode export-only --bundle migration --change-ids add-feature-x # Multi-Tool Workflow (public GitHub + internal ADO) - specfact sync bridge --adapter github --mode export-only --sanitize # Export to public GitHub - specfact sync bridge --adapter github --mode bidirectional --bundle internal --backlog-ids 123 # Import to bundle - specfact sync bridge --adapter ado --mode export-only --bundle internal --change-ids <change-id> # Export to ADO + specfact project sync bridge --adapter github --mode export-only --sanitize # Export to public GitHub + specfact project sync bridge --adapter github --mode bidirectional --bundle internal --backlog-ids 123 # Import to bundle + specfact project sync bridge --adapter ado --mode export-only --bundle internal --change-ids <change-id> # Export to ADO **Finding Change IDs:** diff --git a/packages/specfact-project/src/specfact_project/sync_runtime/sync_perform_operation_impl.py b/packages/specfact-project/src/specfact_project/sync_runtime/sync_perform_operation_impl.py index 691c9312..e6bb166b 100644 --- a/packages/specfact-project/src/specfact_project/sync_runtime/sync_perform_operation_impl.py +++ b/packages/specfact-project/src/specfact_project/sync_runtime/sync_perform_operation_impl.py @@ -34,7 +34,7 @@ def _pso_detect_adapter(repo: Path, adapter_type: AdapterType, console: Any) -> if not adapter_instance.detect(repo, None): console.print(f"[bold red]✗[/bold red] Not a {adapter_type.value} repository") console.print(f"[dim]Expected: {adapter_type.value} structure[/dim]") - console.print("[dim]Tip: Use 'specfact sync bridge probe' to auto-detect tool configuration[/dim]") + console.print("[dim]Tip: Use 'specfact project sync bridge probe' to auto-detect tool configuration[/dim]") raise typer.Exit(1) console.print(f"[bold green]✓[/bold green] Detected {adapter_type.value} repository") return adapter_instance @@ -53,7 +53,7 @@ def _pso_validate_constitution_required( console.print("\n[bold yellow]Next Steps:[/bold yellow]") console.print("1. Run 'specfact sdd constitution bootstrap --repo .' to auto-generate constitution") console.print("2. Or run tool-specific constitution command in your AI assistant") - console.print("3. Then run 'specfact sync bridge --adapter <adapter>' again") + console.print("3. Then run 'specfact project sync bridge --adapter <adapter>' again") raise typer.Exit(1) @@ -130,7 +130,7 @@ def _pso_require_features_for_uni( ) console.print("\n[bold yellow]Next Steps:[/bold yellow]") console.print(f"1. Create feature specifications in your {adapter_type.value} project") - console.print(f"2. Then run 'specfact sync bridge --adapter {adapter_type.value}' again") + console.print(f"2. Then run 'specfact project sync bridge --adapter {adapter_type.value}' again") console.print( f"\n[dim]Note: For bidirectional sync, {adapter_type.value} artifacts are optional if syncing from SpecFact → {adapter_type.value}[/dim]" ) diff --git a/packages/specfact-spec/module-package.yaml b/packages/specfact-spec/module-package.yaml index 65d1d255..f5040d0e 100644 --- a/packages/specfact-spec/module-package.yaml +++ b/packages/specfact-spec/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-spec -version: 0.40.18 +version: 0.40.21 commands: - spec tier: official @@ -21,5 +21,5 @@ description: Official SpecFact specification bundle package. category: spec bundle_group_command: spec integrity: - checksum: sha256:63731fe62322e59382430ddc27eeb372e0f4262b85e3351fce2503d658c042c9 - signature: bjIhapC/QeYOFtkGPS8aHIYc8S3ze4Xkv2XoXAJMf3OBu+uYC37Ytyh07wY6j1X9Gw9z+Ddj8u01f0gxxPO4Bw== + checksum: sha256:95117af3b79e184ffb84e231c8796beaa2cd4f44173a719a356258deb1b5046e + signature: MthYcByRkxA5UTRsdDx4fVZ6akRKsTTr51SJXM2gzxDmixTJiojCPduaWl6OzXdNfLuwrqoTbs1NktK5bxJJAw== diff --git a/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md b/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md index c393f272..51c7fecd 100644 --- a/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md @@ -111,13 +111,11 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: ## Available CLI Commands -- `specfact plan init <bundle-name>` - Initialize project bundle -- `specfact plan select <bundle-name>` - Set active plan (used as default for other commands) -- `specfact code import [<bundle-name>] --repo <path>` - Import from codebase (uses active plan if bundle not specified) -- `specfact plan review [<bundle-name>]` - Review plan (uses active plan if bundle not specified) -- `specfact plan harden [<bundle-name>]` - Create SDD manifest (uses active plan if bundle not specified) -- `specfact govern enforce sdd [<bundle-name>]` - Validate SDD (uses active plan if bundle not specified) -- `specfact sync bridge --adapter <adapter> --repo <path>` - Sync with external tools +- `specfact code import from-code --repo <path> <bundle-name>` - Create or refresh a project bundle from code +- `specfact project health-check --repo <path> --project-name <bundle-name>` - Verify project bundle health +- `specfact project export --repo <path> --bundle <bundle-name> --stdout` - Review project bundle content +- `specfact govern enforce sdd <bundle-name>` - Validate SDD for a project bundle +- `specfact project sync bridge --adapter <adapter> --repo <path>` - Sync with external tools - See [Command Reference](../../docs/reference/commands.md) for full list -**Note**: Most commands now support active plan fallback. If `--bundle` is not specified, commands automatically use the active plan set via `plan select`. This improves workflow efficiency in AI IDE environments. +**Note**: Most commands now support project bundle fallback. If `--bundle` is not specified, inspect the current `specfact project --help` output and use the command-specific bundle or project-name option shown there. diff --git a/packages/specfact-spec/resources/prompts/specfact.07-contracts.md b/packages/specfact-spec/resources/prompts/specfact.07-contracts.md index 7de1d4cb..8c8c1aa1 100644 --- a/packages/specfact-spec/resources/prompts/specfact.07-contracts.md +++ b/packages/specfact-spec/resources/prompts/specfact.07-contracts.md @@ -28,7 +28,7 @@ Complete contract enhancement workflow: analyze coverage → generate prompts ### Target/Input -- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan (set via `plan select`) +- `bundle NAME` (optional argument) - Project bundle name (e.g., legacy-api, auth-module). Default: active plan from repository config - `--repo PATH` - Repository path. Default: current directory (.) - `--apply CONTRACTS` - Contract types to apply: 'all-contracts', 'beartype', 'icontract', 'crosshair', or comma-separated list. Default: 'all-contracts' - `--min-priority PRIORITY` - Minimum priority for files to process: 'high', 'medium', 'low'. Default: 'low' (process all files missing contracts) diff --git a/packages/specfact-spec/src/specfact_spec/generate/commands.py b/packages/specfact-spec/src/specfact_spec/generate/commands.py index d8fe6452..103841ff 100644 --- a/packages/specfact-spec/src/specfact_spec/generate/commands.py +++ b/packages/specfact-spec/src/specfact_spec/generate/commands.py @@ -1573,7 +1573,7 @@ def generate_fix_prompt( 2. Run `specfact generate fix-prompt GAP-001` to get a fix prompt 3. Copy the prompt to your AI IDE 4. AI IDE provides the fix - 5. Validate with `specfact enforce sdd --bundle <bundle>` + 5. Validate with `specfact govern enforce sdd --bundle <bundle>` **Parameter Groups:** - **Target/Input**: gap_id (optional argument), --bundle @@ -1794,7 +1794,7 @@ def generate_fix_prompt( "1. **Analyze the Gap**: Understand what's missing or incorrect", "2. **Implement Fix**: Apply the appropriate fix", "3. **Add Tests**: Ensure the fix is covered by tests", - "4. **Validate**: Run `specfact enforce sdd` to verify", + "4. **Validate**: Run `specfact govern enforce sdd` to verify", ] ) @@ -1810,9 +1810,9 @@ def generate_fix_prompt( ) if bundle: - prompt_parts.append(f"specfact enforce sdd --bundle {bundle}") + prompt_parts.append(f"specfact govern enforce sdd --bundle {bundle}") else: - prompt_parts.append("specfact enforce sdd --bundle <bundle-name>") + prompt_parts.append("specfact govern enforce sdd --bundle <bundle-name>") prompt_parts.extend( [ @@ -1838,7 +1838,7 @@ def generate_fix_prompt( console.print("1. Open the prompt file in your AI IDE (Cursor, Copilot, etc.)") console.print("2. Copy the prompt and ask your AI to implement the fix") console.print("3. Review and apply the suggested changes") - console.print("4. Validate with `specfact enforce sdd`") + console.print("4. Validate with `specfact govern enforce sdd`") if is_debug_mode(): debug_log_operation( diff --git a/packages/specfact-spec/src/specfact_spec/sdd/commands.py b/packages/specfact-spec/src/specfact_spec/sdd/commands.py index 4c25f3d8..c8a83879 100644 --- a/packages/specfact-spec/src/specfact_spec/sdd/commands.py +++ b/packages/specfact-spec/src/specfact_spec/sdd/commands.py @@ -257,7 +257,7 @@ def constitution_bootstrap( console.print("1. Review the generated constitution") console.print("2. Adjust principles and sections as needed") console.print("3. Run 'specfact sdd constitution validate' to check completeness") - console.print("4. Run 'specfact sync bridge --adapter speckit' to sync with Spec-Kit artifacts") + console.print("4. Run 'specfact project sync bridge --adapter speckit' to sync with Spec-Kit artifacts") @constitution_app.command("enrich") diff --git a/pyproject.toml b/pyproject.toml index 39d19042..4b87be4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ scan-all = "semgrep --config packages/specfact-code-review/.semgrep/clean_code.y yaml-lint = "python tools/validate_repo_manifests.py" validate-cli-contracts = "python tools/validate_cli_contracts.py" validate-prompt-commands = "python scripts/check-prompt-commands.py" +generate-command-overview = "python tools/ensure_core_dependency.py && python scripts/generate-command-overview.py --write" +check-command-overview = "python tools/ensure_core_dependency.py && python scripts/generate-command-overview.py --check" +check-command-contract = "python tools/ensure_core_dependency.py && python scripts/check-command-contract.py" check-bundle-imports = "python scripts/check-bundle-imports.py" sign-modules = "python scripts/sign-modules.py {args}" verify-modules-signature = "python scripts/verify-modules-signature.py {args}" @@ -63,7 +66,7 @@ link-dev-module = "python scripts/link_dev_module.py {args}" smart-test = "python tools/smart_test_coverage.py run {args}" smart-test-status = "python tools/smart_test_coverage.py status" smart-test-check = "python tools/smart_test_coverage.py check" -contract-test = "python tools/contract_first_smart_test.py run {args}" +contract-test = "python tools/contract_first_smart_test.py contracts {args}" contract-test-contracts = "python tools/contract_first_smart_test.py contracts {args}" contract-test-exploration = "python tools/contract_first_smart_test.py exploration {args}" contract-test-scenarios = "python tools/contract_first_smart_test.py scenarios {args}" diff --git a/scripts/check-command-contract.py b/scripts/check-command-contract.py new file mode 100644 index 00000000..f31c0310 --- /dev/null +++ b/scripts/check-command-contract.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# ruff: noqa: N999 +"""Validate generated module command overview paths against source Typer apps.""" + +from __future__ import annotations + +import argparse +import importlib +import json +import os +import sys +from pathlib import Path +from typing import Any, cast + +import typer +from typer.testing import CliRunner + + +REPO_ROOT = Path(__file__).resolve().parents[1] +COMMANDS_JSON = REPO_ROOT / "docs" / "reference" / "commands.generated.json" +APP_MOUNTS = ( + ("specfact_backlog.backlog.commands", "app", ("specfact", "backlog")), + ("specfact_codebase.code.commands", "app", ("specfact", "code")), + ("specfact_code_review.review.commands", "app", ("specfact", "code", "review")), + ("specfact_govern.govern.commands", "app", ("specfact", "govern")), + ("specfact_project.project.commands", "app", ("specfact", "project")), + ("specfact_spec.spec.commands", "app", ("specfact", "spec")), +) +MISSING_MARKERS = ( + "missing", + "requires an argument", + "no such option", + "no such command", + "not a valid command", +) + + +def _paired_worktree_repo(source_marker: str, target_marker: str) -> Path | None: + parts = REPO_ROOT.parts + if source_marker not in parts: + return None + marker_index = parts.index(source_marker) + base = Path(*parts[:marker_index]) + suffix = Path(*parts[marker_index + 1 :]) + return base / target_marker / suffix + + +def _ensure_package_paths() -> None: + configured_core_repo = os.environ.get("SPECFACT_CLI_REPO", "").strip() + core_repo_candidates: list[Path | None] = [ + Path(configured_core_repo).expanduser() if configured_core_repo else None, + REPO_ROOT.parent / "specfact-cli", + _paired_worktree_repo("specfact-cli-modules-worktrees", "specfact-cli-worktrees"), + ] + for candidate in core_repo_candidates: + if candidate is None: + continue + src_path = candidate / "src" + if src_path.is_dir(): + src = str(src_path.resolve()) + if src not in sys.path: + sys.path.insert(0, src) + break + for src_path in sorted((REPO_ROOT / "packages").glob("*/src")): + src = str(src_path) + if src not in sys.path: + sys.path.insert(0, src) + + +def _load_apps() -> dict[tuple[str, ...], object]: + _ensure_package_paths() + apps: dict[tuple[str, ...], object] = {} + for module_path, attr_name, prefix in APP_MOUNTS: + module = importlib.import_module(module_path) + apps[prefix] = getattr(module, attr_name) + return apps + + +def _load_records() -> list[dict[str, Any]]: + raw = json.loads(COMMANDS_JSON.read_text(encoding="utf-8")) + if not isinstance(raw, list): + raise ValueError(f"{COMMANDS_JSON} must contain a JSON list") + return [entry for entry in raw if isinstance(entry, dict)] + + +def _command_parts(record: dict[str, Any]) -> list[str]: + command = record.get("command") + return command.split() if isinstance(command, str) else [] + + +def _select_app(apps: dict[tuple[str, ...], object], command_parts: list[str]) -> tuple[object, list[str]] | None: + best_prefix: tuple[str, ...] | None = None + for prefix in apps: + if tuple(command_parts[: len(prefix)]) != prefix: + continue + if best_prefix is None or len(prefix) > len(best_prefix): + best_prefix = prefix + if best_prefix is None: + return None + return apps[best_prefix], command_parts[len(best_prefix) :] + + +def _invoke( + runner: CliRunner, + apps: dict[tuple[str, ...], object], + command_parts: list[str], + suffix: list[str], +) -> tuple[int, str]: + selected = _select_app(apps, command_parts) + if selected is None: + return 2, f"No source app registered for generated command: {' '.join(command_parts)}" + app, args = selected + result = runner.invoke(cast(typer.Typer, app), [*args, *suffix]) + stdout = getattr(result, "stdout", "") or "" + try: + stderr = getattr(result, "stderr", "") or "" + except ValueError: + stderr = "" + return result.exit_code, f"{stdout}{stderr}" + + +def _is_group(record: dict[str, Any]) -> bool: + subcommands = record.get("subcommands") + return isinstance(subcommands, list) and len(subcommands) > 0 + + +def _has_required_argument(record: dict[str, Any]) -> bool: + arguments = record.get("arguments") + if not isinstance(arguments, list): + return False + return any(isinstance(argument, dict) and argument.get("required") for argument in arguments) + + +def _check_help(runner: CliRunner, apps: dict[tuple[str, ...], object], record: dict[str, Any]) -> list[str]: + command_parts = _command_parts(record) + exit_code, output = _invoke(runner, apps, command_parts, ["--help"]) + if exit_code != 0: + return [f"{record.get('command')}: --help exited {exit_code}\n{output}"] + if "usage:" not in output.lower(): + return [f"{record.get('command')}: --help did not render usage\n{output}"] + selected = _select_app(apps, command_parts) + selected_args = selected[1] if selected is not None else [] + if not _is_group(record) and selected_args: + command_parts = _command_parts(record) + usage_lines = [] + capture_usage = False + for line in output.splitlines(): + if "Usage:" in line: + capture_usage = True + if capture_usage: + if not line.strip(): + break + usage_lines.append(line.lower()) + if command_parts and command_parts[-1].lower() not in " ".join(usage_lines): + return [f"{record.get('command')}: --help rendered parent usage instead of leaf usage\n{output}"] + return [] + + +def _check_group_missing_subcommand( + runner: CliRunner, + apps: dict[tuple[str, ...], object], + record: dict[str, Any], +) -> list[str]: + if not _is_group(record) or record.get("bare_invocation") == "executes": + return [] + command_parts = _command_parts(record) + exit_code, output = _invoke(runner, apps, command_parts, []) + normalized = output.lower() + failures: list[str] = [] + if exit_code == 0: + failures.append(f"{record.get('command')}: bare group unexpectedly exited 0") + if "usage:" not in normalized: + failures.append(f"{record.get('command')}: bare group did not render usage") + if "missing subcommand" not in normalized: + failures.append(f"{record.get('command')}: bare group did not explain the missing subcommand") + if normalized.count("usage:") != 1: + failures.append(f"{record.get('command')}: expected exactly one usage block, saw {normalized.count('usage:')}") + if failures: + failures.append(output) + return failures + + +def _check_missing_required_argument( + runner: CliRunner, + apps: dict[tuple[str, ...], object], + record: dict[str, Any], +) -> list[str]: + if _is_group(record) or not _has_required_argument(record): + return [] + command_parts = _command_parts(record) + exit_code, output = _invoke(runner, apps, command_parts, []) + normalized = output.lower() + failures: list[str] = [] + if exit_code == 0: + failures.append(f"{record.get('command')}: missing required argument unexpectedly exited 0") + if "usage:" not in normalized: + failures.append(f"{record.get('command')}: missing required argument did not render usage") + if not any(marker in normalized for marker in MISSING_MARKERS): + failures.append(f"{record.get('command')}: missing required argument did not explain the failure") + if normalized.count("usage:") != 1: + failures.append(f"{record.get('command')}: expected exactly one usage block, saw {normalized.count('usage:')}") + if failures: + failures.append(output) + return failures + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--limit", type=int, default=0, help="Validate only the first N generated commands") + args = parser.parse_args(argv) + + apps = _load_apps() + records = sorted(_load_records(), key=lambda record: len(str(record.get("command", "")).split()), reverse=True) + if args.limit > 0: + records = records[: args.limit] + + runner = CliRunner() + failures: list[str] = [] + for record in records: + failures.extend(_check_help(runner, apps, record)) + failures.extend(_check_group_missing_subcommand(runner, apps, record)) + failures.extend(_check_missing_required_argument(runner, apps, record)) + + if failures: + print("Generated module command contract validation failed:") + print("\n\n".join(failures)) + return 1 + print(f"check-command-contract: OK ({len(records)} generated module command path(s) validated)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-docs-commands.py b/scripts/check-docs-commands.py index 08abb6d0..73805c72 100755 --- a/scripts/check-docs-commands.py +++ b/scripts/check-docs-commands.py @@ -5,11 +5,12 @@ import argparse import importlib +import json import re import sys from collections.abc import Callable from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple, cast from urllib.parse import urlparse import click @@ -56,6 +57,7 @@ "src/specfact_cli/templates", ) WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "docs-review.yml" +GENERATED_COMMANDS_PATH = REPO_ROOT / "docs" / "reference" / "commands.generated.json" MARKDOWN_CODE_RE = re.compile(r"`([^`\n]*specfact [^`\n]*)`") MARKDOWN_LINK_RE = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)") HTML_HREF_RE = re.compile(r'href="([^"]+)"') @@ -77,11 +79,9 @@ class CommandExample(NamedTuple): ("specfact_codebase.code.commands", "app", ("specfact", "code")), ("specfact_code_review.review.commands", "app", ("specfact", "code")), ("specfact_govern.govern.commands", "app", ("specfact", "govern")), - ("specfact_project.import_cmd.commands", "app", ("specfact", "import")), - ("specfact_project.migrate.commands", "app", ("specfact", "migrate")), - ("specfact_project.plan.commands", "app", ("specfact", "plan")), + ("specfact_govern.enforce.commands", "app", ("specfact", "govern", "enforce")), ("specfact_project.project.commands", "app", ("specfact", "project")), - ("specfact_project.sync.commands", "app", ("specfact", "sync")), + ("specfact_spec.contract.commands", "app", ("specfact", "spec", "contract")), ("specfact_spec.spec.commands", "app", ("specfact", "spec")), ("specfact_spec.sdd.commands", "app", ("specfact", "spec")), ("specfact_spec.generate.commands", "app", ("specfact", "spec")), @@ -168,12 +168,31 @@ def _collect_click_paths(group: click.Command, prefix: CommandPath) -> set[Comma def _build_valid_command_paths() -> set[CommandPath]: + if GENERATED_COMMANDS_PATH.is_file(): + try: + raw = json.loads(GENERATED_COMMANDS_PATH.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid generated commands JSON at {GENERATED_COMMANDS_PATH}: {exc}") from exc + if not isinstance(raw, list): + raise ValueError( + f"Unexpected generated commands schema at {GENERATED_COMMANDS_PATH}: expected a JSON list." + ) + generated_paths: set[CommandPath] = set(CORE_COMMAND_PREFIXES) + for entry in raw: + if not isinstance(entry, dict): + raise ValueError(f"Unexpected generated commands entry at {GENERATED_COMMANDS_PATH}: {entry!r}") + command = entry.get("command") + if not isinstance(command, str) or not command.strip(): + raise ValueError(f"Unexpected generated commands entry missing 'command': {entry!r}") + generated_paths.add(tuple(command.split())) + return generated_paths + _ensure_package_paths() paths: set[CommandPath] = set(CORE_COMMAND_PREFIXES) for module_name, attr_name, prefix in MODULE_APP_MOUNTS: module = importlib.import_module(module_name) app = getattr(module, attr_name) - click_group = typer_get_command(app) + click_group = cast(click.Command, typer_get_command(app)) paths.add(prefix) paths.update(_collect_click_paths(click_group, prefix)) return paths @@ -246,12 +265,7 @@ def _normalize_core_docs_route(url: str) -> str | None: def _iter_core_docs_urls_from_text(text: str) -> list[str]: - urls: list[str] = [] - for link in MARKDOWN_LINK_RE.findall(text): - urls.append(link) - for link in HTML_HREF_RE.findall(text): - urls.append(link) - return urls + return [*MARKDOWN_LINK_RE.findall(text), *HTML_HREF_RE.findall(text)] def _core_docs_link_findings_for_line(path: Path, line_number: int, raw_line: str) -> list[ValidationFinding]: diff --git a/scripts/check-prompt-commands.py b/scripts/check-prompt-commands.py index ffed46f4..823fa974 100644 --- a/scripts/check-prompt-commands.py +++ b/scripts/check-prompt-commands.py @@ -9,7 +9,7 @@ import sys from collections.abc import Iterable from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple, cast import click from typer.main import get_command as typer_get_command @@ -17,6 +17,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] PROMPT_ROOT = REPO_ROOT / "packages" +VALIDATED_RESOURCE_SUFFIXES = frozenset({".j2", ".jinja", ".jinja2", ".json", ".md", ".py", ".txt", ".yaml", ".yml"}) INLINE_COMMAND_RE = re.compile(r"`(/?specfact(?:[\s.][^`\n]*)?)`") OPTION_RE = re.compile(r"(?<![\w-])--[A-Za-z][A-Za-z0-9-]*") COMMAND_STARTS = ("specfact ", "/specfact ", "specfact.", "/specfact.") @@ -38,11 +39,7 @@ ("specfact_code_review.review.commands", "app", ("specfact", "code")), ("specfact_govern.govern.commands", "app", ("specfact", "govern")), ("specfact_govern.enforce.commands", "app", ("specfact", "govern", "enforce")), - ("specfact_project.import_cmd.commands", "app", ("specfact", "import")), - ("specfact_project.migrate.commands", "app", ("specfact", "migrate")), - ("specfact_project.plan.commands", "app", ("specfact", "plan")), ("specfact_project.project.commands", "app", ("specfact", "project")), - ("specfact_project.sync.commands", "app", ("specfact", "sync")), ("specfact_spec.contract.commands", "app", ("specfact", "spec", "contract")), ("specfact_spec.spec.commands", "app", ("specfact", "spec")), ("specfact_spec.sdd.commands", "app", ("specfact", "spec")), @@ -92,8 +89,12 @@ def _ensure_package_paths() -> None: def _iter_prompt_paths(root: Path = PROMPT_ROOT) -> list[Path]: paths: list[Path] = [] - for package_root in sorted(root.glob("*/resources/prompts")): - paths.extend(path.resolve() for path in sorted(package_root.rglob("*.md")) if path.is_file()) + for package_root in sorted(root.glob("*/resources")): + paths.extend( + path.resolve() + for path in sorted(package_root.rglob("*")) + if path.is_file() and path.suffix.lower() in VALIDATED_RESOURCE_SUFFIXES + ) return paths @@ -104,18 +105,20 @@ def _load_texts(paths: Iterable[Path]) -> dict[Path, str]: def _command_options(command: click.Command) -> set[str]: options: set[str] = set() for param in command.params: - if isinstance(param, click.Option): + if hasattr(param, "opts"): options.update(opt for opt in param.opts if opt.startswith("--")) - options.update(opt for opt in param.secondary_opts if opt.startswith("--")) + secondary_opts = getattr(param, "secondary_opts", ()) + options.update(opt for opt in secondary_opts if opt.startswith("--")) return options def _collect_click_index(command: click.Command, prefix: tuple[str, ...], index: CommandIndex) -> None: index.command_paths.add(prefix) index.options_by_path.setdefault(prefix, set()).update(_command_options(command)) - if not isinstance(command, click.Group): + children = getattr(command, "commands", None) + if not isinstance(children, dict): return - for name, child in command.commands.items(): + for name, child in children.items(): _collect_click_index(child, (*prefix, name), index) @@ -128,7 +131,7 @@ def _build_command_index() -> CommandIndex: try: module = importlib.import_module(module_name) app = getattr(module, attr_name) - click_command = typer_get_command(app) + click_command = cast(click.Command, typer_get_command(app)) except Exception as exc: msg = f"Failed to load CLI mount {module_name}:{attr_name} at {' '.join(prefix)}: {exc}" raise RuntimeError(msg) from exc @@ -152,7 +155,7 @@ def _strip_shell_prompt(line: str) -> str: def _normalize_prompt_command(raw: str) -> str: - command = raw.strip().rstrip(":,") + command = raw.strip().strip("\"'").rstrip(":,").strip("\"'") if command.startswith("/"): command = command[1:] return " ".join(command.split()) @@ -207,13 +210,39 @@ def _iter_inline_command_examples(text: str, source: Path) -> list[PromptCommand return examples +def _iter_structured_value_command_examples(text: str, source: Path) -> list[PromptCommandExample]: + """Extract commands from YAML/JSON/template scalar values.""" + if source.suffix.lower() == ".md": + return [] + examples: list[PromptCommandExample] = [] + for line_number, raw_line in enumerate(text.splitlines(), start=1): + stripped = _strip_comment(raw_line.strip()).lstrip("-").strip() + if not stripped: + continue + candidates = [stripped] + if ":" in stripped: + candidates.append(stripped.split(":", 1)[1].strip()) + for candidate in candidates: + candidate = candidate.strip().strip(",").strip("\"'") + if not _starts_with_command(candidate): + continue + examples.append(PromptCommandExample(source, line_number, _normalize_prompt_command(candidate))) + break + return examples + + def _extract_prompt_command_examples_from_text(text: str, source: Path) -> list[PromptCommandExample]: seen: set[tuple[int, str]] = set() examples: list[PromptCommandExample] = [] inline_examples = _iter_inline_command_examples(text, source) cli_inline_examples = [example for example in inline_examples if "." not in example.text.split(maxsplit=1)[0]] slash_inline_examples = [example for example in inline_examples if "." in example.text.split(maxsplit=1)[0]] - for example in [*_iter_fenced_command_examples(text, source), *cli_inline_examples, *slash_inline_examples]: + for example in [ + *_iter_fenced_command_examples(text, source), + *_iter_structured_value_command_examples(text, source), + *cli_inline_examples, + *slash_inline_examples, + ]: key = (example.line_number, example.text) if key in seen: continue @@ -347,7 +376,7 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace: "paths", nargs="*", type=Path, - help="Prompt files to validate. Defaults to packages/*/resources/prompts/**/*.md.", + help="Prompt/resource/source files to validate. Defaults to package resources plus packages/*/src/**/*.py.", ) return parser.parse_args(argv) @@ -355,7 +384,9 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace: def _selected_paths(args: argparse.Namespace) -> list[Path]: if not args.paths: return _iter_prompt_paths() - return [path.resolve() for path in args.paths if path.suffix == ".md" and path.is_file()] + return [ + path.resolve() for path in args.paths if path.suffix.lower() in VALIDATED_RESOURCE_SUFFIXES and path.is_file() + ] def _main(argv: list[str] | None = None) -> int: diff --git a/scripts/generate-command-overview.py b/scripts/generate-command-overview.py new file mode 100644 index 00000000..4407d711 --- /dev/null +++ b/scripts/generate-command-overview.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# ruff: noqa: N999 +"""Generate deterministic module command overview artifacts for humans and AI agents.""" + +from __future__ import annotations + +import argparse +import difflib +import importlib +import json +import os +import sys +from pathlib import Path +from typing import Any, cast + +import click +from typer.main import get_command + + +REPO_ROOT = Path(__file__).resolve().parents[1] +JSON_PATH = REPO_ROOT / "docs" / "reference" / "commands.generated.json" +MARKDOWN_PATH = REPO_ROOT / "docs" / "reference" / "commands.generated.md" +LLMS_PATH = REPO_ROOT / "llms.txt" + +MODULE_APP_MOUNTS = ( + ("specfact_backlog.backlog.commands", "app", ("specfact", "backlog"), "nold-ai/specfact-backlog"), + ("specfact_codebase.code.commands", "app", ("specfact", "code"), "nold-ai/specfact-codebase"), + ("specfact_code_review.review.commands", "app", ("specfact", "code", "review"), "nold-ai/specfact-code-review"), + ("specfact_govern.govern.commands", "app", ("specfact", "govern"), "nold-ai/specfact-govern"), + ("specfact_project.project.commands", "app", ("specfact", "project"), "nold-ai/specfact-project"), + ("specfact_spec.spec.commands", "app", ("specfact", "spec"), "nold-ai/specfact-spec"), +) + + +def _paired_worktree_repo(source_marker: str, target_marker: str) -> Path | None: + parts = REPO_ROOT.parts + if source_marker not in parts: + return None + marker_index = parts.index(source_marker) + base = Path(*parts[:marker_index]) + suffix = Path(*parts[marker_index + 1 :]) + return base / target_marker / suffix + + +def _ensure_package_paths() -> None: + configured_core_repo = os.environ.get("SPECFACT_CLI_REPO", "").strip() + core_repo_candidates: list[Path | None] = [ + Path(configured_core_repo).expanduser() if configured_core_repo else None, + REPO_ROOT.parent / "specfact-cli", + _paired_worktree_repo("specfact-cli-modules-worktrees", "specfact-cli-worktrees"), + ] + for candidate in core_repo_candidates: + if candidate is None: + continue + src_path = candidate / "src" + if src_path.is_dir(): + src = str(src_path.resolve()) + if src not in sys.path: + sys.path.insert(0, src) + break + for src_path in sorted((REPO_ROOT / "packages").glob("*/src")): + src = str(src_path) + if src not in sys.path: + sys.path.insert(0, src) + + +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", ()) + options.update(opt for opt in [*param.opts, *secondary_opts] if opt.startswith("--")) + return sorted(options) + + +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"): + arguments.append( + { + "name": param.human_readable_name, + "required": bool(param.required), + "nargs": param.nargs, + } + ) + return arguments + + +def _command_children(command: click.Command) -> dict[str, click.Command]: + if not (hasattr(command, "list_commands") and hasattr(command, "get_command")): + return {} + context_cls = getattr(command, "context_class", click.Context) + with context_cls(command, info_name=command.name) as ctx: + children: dict[str, click.Command] = {} + for name in command.list_commands(ctx): + if name == "__delegate__": + continue + child = command.get_command(ctx, name) + if child is not None: + children[name] = child + return children + + +def _bare_invocation(command: click.Command) -> str: + is_group = hasattr(command, "list_commands") and hasattr(command, "get_command") + if is_group and bool(getattr(command, "invoke_without_command", False)) and _has_bare_business_parameters(command): + return "executes" + if is_group: + return "requires-subcommand" + return "executes" + + +def _has_bare_business_parameters(command: click.Command) -> bool: + ignored_options = { + "--help", + "-h", + "--help-advanced", + "-ha", + "--install-completion", + "--show-completion", + } + for param in command.params: + if not hasattr(param, "opts") and hasattr(param, "human_readable_name"): + return True + if hasattr(param, "opts"): + opts = set(param.opts) | set(getattr(param, "secondary_opts", ())) + if opts and opts.isdisjoint(ignored_options): + return True + return False + + +def _walk(command: click.Command, path: tuple[str, ...], source: str, module_id: str) -> list[dict[str, Any]]: + children = _command_children(command) + record = { + "command": " ".join(path), + "owner_repo": "nold-ai/specfact-cli-modules", + "owner_package": module_id, + "install_prerequisite": f"specfact module install {module_id}", + "short_help": (command.short_help or "").strip(), + "arguments": _command_arguments(command), + "bare_invocation": _bare_invocation(command), + "options": _command_options(command), + "subcommands": sorted(children), + "source": source, + "hidden": bool(getattr(command, "hidden", False)), + "deprecated": bool(getattr(command, "deprecated", False)), + } + records = [record] + for name, child in sorted(children.items()): + records.extend(_walk(child, (*path, name), source, module_id)) + return records + + +def build_records() -> list[dict[str, Any]]: + _ensure_package_paths() + records: list[dict[str, Any]] = [] + for module_name, attr_name, prefix, module_id in MODULE_APP_MOUNTS: + module = importlib.import_module(module_name) + app = getattr(module, attr_name) + click_command = cast(click.Command, get_command(app)) + records.extend(_walk(click_command, prefix, f"{module_name}:{attr_name}", module_id)) + return sorted(records, key=lambda record: record["command"]) + + +def _render_markdown(records: list[dict[str, Any]]) -> str: + lines = [ + "---", + "layout: default", + "title: Generated SpecFact Module Command Overview", + "permalink: /reference/generated-module-command-overview/", + "exempt: true", + "exempt_reason: Generated command contract artifact.", + "---", + "", + "# Generated SpecFact Module Command Overview", + "", + "This file is generated from the current module command trees. Do not edit by hand.", + "", + "| Command | Module | Install | Options | Subcommands | Context |", + "| --- | --- | --- | --- | --- | --- |", + ] + for record in records: + arguments = ", ".join( + f"{arg['name']}{' (required)' if arg.get('required') else ''}" for arg in record["arguments"] + ) + options = ", ".join(record["options"]) or "-" + subcommands = ", ".join(record["subcommands"]) or "-" + help_text = str(record["short_help"]).replace("\n", " ") + lines.append( + f"| `{record['command']}` | {record['owner_package']} | `{record['install_prerequisite']}` | " + f"{options}; args: {arguments or '-'} | {subcommands} | {help_text} |" + ) + lines.append("") + return "\n".join(lines) + + +def _render_llms(markdown: str) -> str: + return "\n".join( + [ + "# SpecFact Module Commands", + "", + "Use this generated overview as the current module command contract " + "before following older docs or prompts.", + "", + markdown, + ] + ) + + +def _desired_outputs() -> dict[Path, str]: + records = build_records() + markdown = _render_markdown(records) + return { + JSON_PATH: json.dumps(records, indent=2, sort_keys=True) + "\n", + MARKDOWN_PATH: markdown, + LLMS_PATH: _render_llms(markdown), + } + + +def _check(outputs: dict[Path, str]) -> int: + failures = [] + for path, expected in outputs.items(): + actual = path.read_text(encoding="utf-8") if path.exists() else "" + if actual != expected: + failures.append(path) + print( + "\n".join( + difflib.unified_diff( + actual.splitlines(), + expected.splitlines(), + fromfile=str(path), + tofile=f"{path} (generated)", + lineterm="", + ) + ) + ) + if failures: + print("Module command overview artifacts are stale. Run: python scripts/generate-command-overview.py --write") + return 1 + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--write", action="store_true", help="Write generated artifacts") + parser.add_argument("--check", action="store_true", help="Check generated artifacts are current") + args = parser.parse_args(argv) + outputs = _desired_outputs() + if args.write: + for path, text in outputs.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return 0 + return _check(outputs) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pre-commit-quality-checks.sh b/scripts/pre-commit-quality-checks.sh index 8edc6fba..9298c541 100755 --- a/scripts/pre-commit-quality-checks.sh +++ b/scripts/pre-commit-quality-checks.sh @@ -38,7 +38,7 @@ print_block2_overview() { echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo " modules pre-commit — Block 2: code review + contract tests (2 stages)" >&2 echo " 1/2 code review gate (staged paths under packages/, registry/, scripts/, tools/, tests/, openspec/changes/)" >&2 - echo " 2/2 contract-first tests (contract-test-status → hatch run contract-test)" >&2 + echo " 2/2 contract-first tests (contract-test-status → hatch run contract-test-contracts)" >&2 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo "" >&2 } @@ -78,7 +78,7 @@ staged_docs_validation_paths() { while IFS= read -r line; do [ -z "${line}" ] && continue case "${line}" in - docs/*|*.md|requirements-docs-ci.txt|scripts/check-docs-commands.py|scripts/docs_site_validation.py) + docs/*|*.md|requirements-docs-ci.txt|scripts/check-docs-commands.py|scripts/check-command-contract.py|scripts/docs_site_validation.py|scripts/generate-command-overview.py|llms.txt) printf '%s\n' "${line}" ;; esac @@ -90,7 +90,7 @@ staged_prompt_validation_paths() { while IFS= read -r line; do [ -z "${line}" ] && continue case "${line}" in - packages/*/resources/prompts/*.md|packages/*/src/**/commands.py|scripts/check-prompt-commands.py|tests/unit/test_check_prompt_commands_script.py) + packages/*/resources/**|packages/*/src/**/commands.py|scripts/check-prompt-commands.py|tests/unit/test_check_prompt_commands_script.py) printf '%s\n' "${line}" ;; esac @@ -133,12 +133,71 @@ run_prompt_command_validation_gate() { if ! needs_prompt_command_validation; then return 0 fi - info "📄 Prompt command validation — running \`hatch run validate-prompt-commands\` (staged bundle prompts or prompt validation tooling)" - if hatch run validate-prompt-commands; then + local validation_paths=() + local full_scan=0 + while IFS= read -r line; do + [ -z "${line}" ] && continue + case "${line}" in + scripts/check-prompt-commands.py|tests/unit/test_check_prompt_commands_script.py) + full_scan=1 + ;; + *) + validation_paths+=("${line}") + ;; + esac + done < <(staged_prompt_validation_paths) + info "📄 Prompt command validation — running command-reference validation for staged prompts/resources/source guidance" + if [[ "${full_scan}" -eq 1 ]]; then + validation_paths=() + fi + if hatch run python scripts/check-prompt-commands.py "${validation_paths[@]}"; then success "✅ Prompt command validation passed" else error "❌ Prompt command validation failed" - warn "💡 Run: hatch run validate-prompt-commands" + warn "💡 Run: hatch run python scripts/check-prompt-commands.py <changed prompt/resource/source paths>" + exit 1 + fi +} + +run_command_overview_validation_gate() { + if ! needs_docs_site_validation && ! needs_prompt_command_validation; then + return 0 + fi + local unstaged_inputs + unstaged_inputs="$( + { + git diff --name-only -- packages scripts/generate-command-overview.py scripts/check-command-contract.py pyproject.toml + git ls-files --others --exclude-standard -- packages scripts/generate-command-overview.py scripts/check-command-contract.py pyproject.toml + } | sort -u + )" + if [[ -n "${unstaged_inputs}" ]]; then + error "❌ Command overview inputs have unstaged changes; refusing to auto-stage generated artifacts" + warn "Stage or stash these paths before committing:" + printf '%s\n' "${unstaged_inputs}" >&2 + exit 1 + fi + info "📄 Command overview validation — regenerating current AI command artifacts" + if hatch run generate-command-overview; then + git add -- llms.txt docs/reference/commands.generated.json docs/reference/commands.generated.md + success "✅ Command overview artifacts regenerated and staged" + else + error "❌ Command overview generation failed" + exit 1 + fi + info "📄 Command overview validation — running \`hatch run check-command-overview\`" + if hatch run check-command-overview; then + success "✅ Command overview validation passed" + else + error "❌ Command overview validation failed" + warn "💡 Run: hatch run generate-command-overview" + exit 1 + fi + info "📄 Command contract validation — running \`hatch run check-command-contract\`" + if hatch run check-command-contract; then + success "✅ Generated command contract validation passed" + else + error "❌ Generated command contract validation failed" + warn "💡 Run: hatch run check-command-contract" exit 1 fi } @@ -242,7 +301,8 @@ run_code_review_gate() { return fi - info "📦 Block 2 — stage 1/2: code review — running \`hatch run python scripts/pre_commit_code_review.py\` (${#review_array[@]} path(s))" + local enforcement="${SPECFACT_CODE_REVIEW_ENFORCEMENT:-changed}" + info "📦 Block 2 — stage 1/2: code review — running \`hatch run python scripts/pre_commit_code_review.py\` (${#review_array[@]} path(s), enforcement=${enforcement})" if hatch run python scripts/pre_commit_code_review.py "${review_array[@]}"; then success "✅ Block 2 — stage 1/2: code review gate passed" else @@ -257,13 +317,13 @@ run_contract_tests_visible() { if hatch run contract-test-status > /dev/null 2>&1; then success "✅ Block 2 — stage 2/2: contract tests — skipped (contract-test-status: no input changes)" else - info "📦 Block 2 — stage 2/2: contract tests — running \`hatch run contract-test\`" - if hatch run contract-test; then + info "📦 Block 2 — stage 2/2: contract tests — running \`hatch run contract-test-contracts\`" + if hatch run contract-test-contracts; then success "✅ Block 2 — stage 2/2: contract-first tests passed" warn "💡 CI may still run the full quality matrix" else error "❌ Block 2 — stage 2/2: contract-first tests failed" - warn "💡 Run: hatch run contract-test-status" + warn "💡 Run: hatch run contract-test-contracts" exit 1 fi fi @@ -292,6 +352,7 @@ run_block1_lint() { run_block2() { warn "🔍 modules pre-commit — Block 2 — hook: review + contract tests" + run_command_overview_validation_gate run_docs_site_validation_gate run_prompt_command_validation_gate if check_safe_change; then @@ -312,6 +373,7 @@ run_all() { run_bundle_import_checks run_lint_if_staged_python success "✅ Block 1 complete (all stages passed or skipped as expected)" + run_command_overview_validation_gate run_docs_site_validation_gate run_prompt_command_validation_gate if check_safe_change; then diff --git a/scripts/pre_commit_code_review.py b/scripts/pre_commit_code_review.py index 22d5c322..19d37f54 100755 --- a/scripts/pre_commit_code_review.py +++ b/scripts/pre_commit_code_review.py @@ -16,6 +16,7 @@ import importlib.util import json import os +import re import subprocess import sys from collections.abc import Callable, Sequence @@ -47,6 +48,8 @@ def _load_dev_bootstrap() -> Any: # Default matches dogfood / OpenSpec: machine-readable report under ignored ``.specfact/``. REVIEW_JSON_OUT = ".specfact/code-review.json" +VALID_ENFORCEMENT_MODES = frozenset({"full", "changed", "shadow"}) +DEFAULT_ENFORCEMENT_MODE = "changed" def _is_review_gate_path(path: str) -> bool: @@ -98,12 +101,25 @@ def _specfact_review_paths(paths: Sequence[str]) -> list[str]: return result +def review_enforcement_mode() -> str: + """Return configured pre-commit review enforcement mode.""" + configured = os.environ.get("SPECFACT_CODE_REVIEW_ENFORCEMENT", DEFAULT_ENFORCEMENT_MODE).strip().lower() + if configured in VALID_ENFORCEMENT_MODES: + return configured + sys.stderr.write( + "Invalid SPECFACT_CODE_REVIEW_ENFORCEMENT value " + f"{configured!r}; expected one of: {', '.join(sorted(VALID_ENFORCEMENT_MODES))}.\n" + ) + return DEFAULT_ENFORCEMENT_MODE + + @require(lambda files: files is not None) @ensure(lambda result: result[:5] == [sys.executable, "-m", "specfact_cli.cli", "code", "review"]) @ensure(lambda result: "--json" in result and "--out" in result) @ensure(lambda result: REVIEW_JSON_OUT in result) -def build_review_command(files: Sequence[str]) -> list[str]: +def build_review_command(files: Sequence[str], *, enforcement: str | None = None) -> list[str]: """Build ``code review run --json --out …`` so findings are written for tooling.""" + mode = enforcement or review_enforcement_mode() return [ sys.executable, "-m", @@ -114,6 +130,8 @@ def build_review_command(files: Sequence[str]) -> list[str]: "--json", "--out", REVIEW_JSON_OUT, + "--enforcement", + mode, *files, ] @@ -141,6 +159,8 @@ def _run_review_subprocess( cmd: list[str], repo_root: Path, files: Sequence[str], + *, + enforcement: str, ) -> subprocess.CompletedProcess[str] | None: """Run the nested SpecFact review command and handle timeout reporting.""" env = os.environ.copy() @@ -149,11 +169,16 @@ def _run_review_subprocess( # shadow in-repo `specfact_code_review` during the pre-commit gate. env["SPECFACT_MODULES_REPO"] = str(repo_root.resolve()) env["SPECFACT_CLI_MODULES_REPO"] = str(repo_root.resolve()) - code_review_src = repo_root / "packages" / "specfact-code-review" / "src" - if code_review_src.is_dir(): - prefix = str(code_review_src) - previous = env.get("PYTHONPATH", "").strip() - env["PYTHONPATH"] = f"{prefix}{os.pathsep}{previous}" if previous else prefix + env["SPECFACT_MODULES_ROOTS"] = str((repo_root / "packages").resolve()) + package_src_roots = [path / "src" for path in sorted((repo_root / "packages").glob("specfact-*"))] + prefixes = [str(path) for path in package_src_roots if path.is_dir()] + previous = env.get("PYTHONPATH", "").strip() + if previous: + prefixes.extend(entry for entry in previous.split(os.pathsep) if entry) + if prefixes: + env["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(prefixes)) + if enforcement == "changed": + env["SPECFACT_CODE_REVIEW_CHANGED_DIFF"] = "cached" try: return subprocess.run( cmd, @@ -233,41 +258,135 @@ def _count_ai_bloat_findings(findings: list[object]) -> int: return count -def _print_review_findings_summary(repo_root: Path) -> tuple[bool, int | None, int | None]: - """Parse ``REVIEW_JSON_OUT``, print counts, return ``(ok, error_count, ci_exit_code)``. +def _repo_relative_report_path(repo_root: Path, raw_path: object) -> str | None: + """Normalize a finding path to a repository-relative POSIX path.""" + if not isinstance(raw_path, str) or not raw_path.strip(): + return None + path = Path(raw_path) + if path.is_absolute(): + try: + path = path.relative_to(repo_root) + except ValueError: + return None + return path.as_posix() + + +def _parse_added_lines_from_cached_diff(diff_text: str) -> dict[str, set[int]]: + """Return staged new-line numbers by repo-relative file from a zero-context diff.""" + changed_lines: dict[str, set[int]] = {} + current_file: str | None = None + previous_was_source_header = False + for line in diff_text.splitlines(): + if line.startswith("--- "): + previous_was_source_header = True + continue + if previous_was_source_header and line.startswith("+++ "): + previous_was_source_header = False + destination = line[4:].strip() + current_file = None if destination == "/dev/null" else destination.removeprefix("b/") + if current_file is not None: + changed_lines.setdefault(current_file, set()) + continue + previous_was_source_header = False + if current_file is None or not line.startswith("@@ "): + continue + match = re.search(r"\+(\d+)(?:,(\d+))?", line) + if match is None: + continue + start = int(match.group(1)) + count = int(match.group(2) or "1") + if count > 0: + changed_lines[current_file].update(range(start, start + count)) + return changed_lines + + +def _staged_changed_lines(repo_root: Path) -> dict[str, set[int]]: + """Collect staged new-line numbers so legacy file findings do not block unrelated commits.""" + completed = subprocess.run( + ["git", "diff", "--cached", "--unified=0", "--no-ext-diff"], + cwd=repo_root, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + return {} + return _parse_added_lines_from_cached_diff(completed.stdout) - Callers should use ``ci_exit_code`` as the hook exit code; ``error_count`` is informational only - because fixable error-severity findings may still yield a passing ``ci_exit_code``. - """ + +def _finding_is_blocking(item: object) -> bool: + """Return whether a raw finding has blocking review semantics.""" + if not isinstance(item, dict): + return False + severity = item.get("severity") + return isinstance(severity, str) and severity.lower().strip() == "error" and item.get("fixable") is not True + + +def _finding_targets_staged_line(repo_root: Path, item: object, changed_lines: dict[str, set[int]]) -> bool: + """Return whether a finding points at a staged changed line.""" + if not isinstance(item, dict): + return False + relative_path = _repo_relative_report_path(repo_root, item.get("file")) + if relative_path is None or relative_path not in changed_lines: + return False + line_number = item.get("line") + if isinstance(line_number, int): + return line_number in changed_lines[relative_path] + return bool(changed_lines[relative_path]) + + +def _load_review_report(repo_root: Path) -> dict[str, Any] | None: + """Load the review report JSON object, printing a precise diagnostic on failure.""" report_path = _report_path(repo_root) if not report_path.is_file(): sys.stderr.write(f"Code review: no report file at {REVIEW_JSON_OUT} (could not print findings summary).\n") - return False, None, None + return None try: data = json.loads(report_path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError) as exc: sys.stderr.write(f"Code review: could not read {REVIEW_JSON_OUT}: {exc}\n") - return False, None, None + return None except json.JSONDecodeError as exc: sys.stderr.write(f"Code review: invalid JSON in {REVIEW_JSON_OUT}: {exc}\n") - return False, None, None - + return None if not isinstance(data, dict): sys.stderr.write(f"Code review: expected top-level JSON object in {REVIEW_JSON_OUT}.\n") - return False, None, None + return None + return cast(dict[str, Any], data) - findings_raw = data.get("findings") - if not isinstance(findings_raw, list): - sys.stderr.write(f"Code review: report has no findings list in {REVIEW_JSON_OUT}.\n") - return False, None, None - counts = count_findings_by_severity(findings_raw) - ai_bloat_count = _count_ai_bloat_findings(findings_raw) - total = len(findings_raw) - verdict = data.get("overall_verdict", "?") - ci_exit_code = data.get("ci_exit_code") - if ci_exit_code not in {0, 1}: - ci_exit_code = 1 if verdict == "FAIL" else 0 +def _raw_ci_exit_code(report: dict[str, Any]) -> int: + """Read the report CI exit code, deriving it from the verdict when missing.""" + raw = report.get("ci_exit_code") + if raw in {0, 1}: + return int(raw) + return 1 if report.get("overall_verdict") == "FAIL" else 0 + + +def _changed_line_blockers(repo_root: Path, findings_raw: list[object]) -> list[object]: + """Return blocking findings that point at staged changed lines.""" + changed_lines = _staged_changed_lines(repo_root) + return [ + item + for item in findings_raw + if _finding_is_blocking(item) and _finding_targets_staged_line(repo_root, item, changed_lines) + ] + + +def _enforced_exit_code( + repo_root: Path, findings_raw: list[object], *, enforcement: str, raw_ci_exit_code: int +) -> tuple[int, list[object]]: + """Apply configured enforcement to the raw report exit code.""" + if enforcement == "full": + return raw_ci_exit_code, [] + if enforcement == "shadow": + return 0, [] + blockers = _changed_line_blockers(repo_root, findings_raw) + return (1 if blockers else 0), blockers + + +def _finding_summary_parts(counts: dict[str, int], *, ai_bloat_count: int) -> list[str]: + """Format finding count buckets for concise stderr output.""" parts = [ f"errors={counts['error']}", f"warnings={counts['warning']}", @@ -279,8 +398,63 @@ def _print_review_findings_summary(repo_root: Path) -> tuple[bool, int | None, i parts.append(f"other={counts['other']}") if ai_bloat_count: parts.append(f"ai_bloat={ai_bloat_count}") - summary = ", ".join(parts) + return parts + + +def _print_enforcement_summary( + *, + enforcement: str, + raw_ci_exit_code: int, + ci_exit_code: int, + blocking_changed_findings: list[object], +) -> None: + """Print the enforcement decision evidence.""" + sys.stderr.write(f"Code review enforcement: {enforcement}.\n") + if enforcement == "shadow" and raw_ci_exit_code == 1: + sys.stderr.write("Code review shadow gate: findings are evidence-only and do not block.\n") + elif raw_ci_exit_code == 1 and ci_exit_code == 0: + sys.stderr.write( + "Code review changed-line gate: no blocking findings target staged lines; " + "legacy findings remain in the JSON report.\n" + ) + elif blocking_changed_findings: + sys.stderr.write( + f"Code review changed-line gate: {len(blocking_changed_findings)} blocking finding(s) target staged lines.\n" + ) + + +def _print_review_findings_summary(repo_root: Path, *, enforcement: str) -> tuple[bool, int | None, int | None]: + """Parse ``REVIEW_JSON_OUT``, print counts, return ``(ok, error_count, ci_exit_code)``. + + Callers should use ``ci_exit_code`` as the hook exit code; ``error_count`` is informational only + because fixable error-severity findings may still yield a passing ``ci_exit_code``. + """ + data = _load_review_report(repo_root) + if data is None: + return False, None, None + + findings_raw = data.get("findings") + if not isinstance(findings_raw, list): + sys.stderr.write(f"Code review: report has no findings list in {REVIEW_JSON_OUT}.\n") + return False, None, None + + counts = count_findings_by_severity(findings_raw) + ai_bloat_count = _count_ai_bloat_findings(findings_raw) + total = len(findings_raw) + verdict = data.get("overall_verdict", "?") + raw_ci_exit_code = _raw_ci_exit_code(data) + ci_exit_code, blocking_changed_findings = _enforced_exit_code( + repo_root, findings_raw, enforcement=enforcement, raw_ci_exit_code=raw_ci_exit_code + ) + summary = ", ".join(_finding_summary_parts(counts, ai_bloat_count=ai_bloat_count)) sys.stderr.write(f"Code review summary: {total} finding(s) ({summary}); overall_verdict={verdict!r}.\n") + _print_enforcement_summary( + enforcement=enforcement, + raw_ci_exit_code=raw_ci_exit_code, + ci_exit_code=ci_exit_code, + blocking_changed_findings=blocking_changed_findings, + ) + report_path = _report_path(repo_root) abs_report = report_path.resolve() sys.stderr.write(f"Code review report file: {REVIEW_JSON_OUT}\n") sys.stderr.write(f" absolute path: {abs_report}\n") @@ -341,16 +515,17 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 repo_root = _repo_root() - cmd = build_review_command(specfact_files) + enforcement = review_enforcement_mode() + cmd = build_review_command(specfact_files, enforcement=enforcement) report_path = _prepare_report_path(repo_root) - result = _run_review_subprocess(cmd, repo_root, specfact_files) + result = _run_review_subprocess(cmd, repo_root, specfact_files, enforcement=enforcement) if result is None: return 1 if not report_path.is_file(): return _missing_report_exit_code(report_path, result) # Do not echo nested `specfact code review run` stdout/stderr (verbose tool banners); full report # is in REVIEW_JSON_OUT; we print a short summary on stderr below. - summary_ok, _error_count, ci_exit_code = _print_review_findings_summary(repo_root) + summary_ok, _error_count, ci_exit_code = _print_review_findings_summary(repo_root, enforcement=enforcement) if not summary_ok or ci_exit_code is None: return 1 return int(ci_exit_code) diff --git a/skills/specfact-code-review/SKILL.md b/skills/specfact-code-review/SKILL.md index 7f5bcc15..9df5bf08 100644 --- a/skills/specfact-code-review/SKILL.md +++ b/skills/specfact-code-review/SKILL.md @@ -9,11 +9,12 @@ allowed-tools: [] Updated: 2026-05-22 | Module: nold-ai/specfact-code-review Use this skill as an interactive cleanup coach, not a raw lint executor. When a user says "remove AI bloat", "simplify", "apply clean code", "fix SpecFact review", or similar, run the SpecFact review workflow, explain decisions in the user's language, show exact patch previews, and validate after small changes. +Operating guidance: command examples in this skill are not the source of truth; CLI help is authoritative. Check `specfact code review run --help`, and ask the user before guessing when help output disagrees. ## DO - Treat `specfact code review run --help` as authoritative; use `--instructions` as the fallback AI workflow when prompts/skills are unavailable -- For simplification queues, run `specfact code review run --scope changed --focus simplify --json --out .specfact/code-review-simplify.json` +- For simplification queues, run `specfact code review run --scope changed --enforcement shadow --focus simplify --json --out .specfact/code-review-simplify.json` - Ask for walkthrough level when interactive: vibe coder, junior developer, senior/pro, or headless agent; auto-adjust if obvious - For vibe coders, present each finding as a decision card: plain-language issue, why it might need to stay, exact patch preview, validation plan, and recommended choice - Interpret `guidance_kind`: `safe_mechanical` may apply after local safety checks, `needs_tests` requires tests first, `design_judgment` needs human choice with intent evidence, `preserve` means keep and log `preserve_reason` @@ -21,7 +22,7 @@ Use this skill as an interactive cleanup coach, not a raw lint executor. When a - Log each simplification action as recommended, applied, kept, skipped, failed, with evidence of improvement or preserved contract - In headless mode, process one file at a time and emit an action table: file, line, rule, guidance_kind, recommended_action, action_status, evidence - Run targeted tests or rerun simplify review after each accepted file or very small batch; if validation cannot prove safety, downgrade to `needs_tests` or `skipped` -- For merge-quality review, run `specfact code review run --scope changed --bug-hunt --json --out .specfact/code-review.json` +- For merge-quality review, run `specfact code review run --scope changed --enforcement changed --bug-hunt --json --out .specfact/code-review.json`; use `--enforcement full` only when the user wants legacy blockers in reviewed files to fail - Ask whether tests should be included before repo-wide review; default to excluding tests unless test changes are the target - Use intention-revealing names; avoid placeholder public names like data/process/handle - Keep functions under 120 LOC, shallow nesting, and <= 5 parameters (KISS) diff --git a/tests/cli-contracts/specfact-code-review-run.scenarios.yaml b/tests/cli-contracts/specfact-code-review-run.scenarios.yaml index e5cd60e8..754fa593 100644 --- a/tests/cli-contracts/specfact-code-review-run.scenarios.yaml +++ b/tests/cli-contracts/specfact-code-review-run.scenarios.yaml @@ -66,6 +66,39 @@ scenarios: exit_code: 0 stdout_contains: - review-report.json + - name: enforcement-shadow-dirty-exit-zero + type: pattern + argv: + - --json + - --enforcement + - shadow + - tests/fixtures/review/dirty_module.py + expect: + exit_code: 0 + stdout_contains: + - review-report.json + - name: mode-and-enforcement-are-rejected + type: anti-pattern + argv: + - --mode + - shadow + - --enforcement + - changed + expect: + exit_code: 2 + stderr_contains: + - Use only one of --mode or --enforcement + - name: enforcement-full-dirty-exit-nonzero + type: pattern + argv: + - --json + - --enforcement + - full + - tests/fixtures/review/dirty_module.py + expect: + exit_code: 1 + stdout_contains: + - review-report.json - name: bug-hunt-shadow-dirty-exit-zero type: pattern argv: @@ -150,6 +183,8 @@ scenarios: - --out - CONTRACT_TMP_REPORT.json - --bug-hunt + - --enforcement + - full - --level - error - tests/fixtures/review/dirty_module.py diff --git a/tests/e2e/specfact_project/test_help_smoke.py b/tests/e2e/specfact_project/test_help_smoke.py index 89dfdafd..db0c9c6a 100644 --- a/tests/e2e/specfact_project/test_help_smoke.py +++ b/tests/e2e/specfact_project/test_help_smoke.py @@ -18,3 +18,12 @@ def test_project_sync_bridge_help_smoke() -> None: project_app = importlib.import_module("specfact_project.project.commands").app result = runner.invoke(project_app, ["sync", "bridge", "--help"]) assert result.exit_code == 0 + + +def test_project_sync_bridge_help_uses_canonical_command_path() -> None: + project_app = importlib.import_module("specfact_project.project.commands").app + result = runner.invoke(project_app, ["sync", "bridge", "--help"]) + + assert result.exit_code == 0 + assert "specfact project sync bridge" in result.stdout + assert "specfact sync bridge" not in result.stdout diff --git a/tests/unit/docs/test_code_review_docs_parity.py b/tests/unit/docs/test_code_review_docs_parity.py index 9bcac083..9325e52c 100644 --- a/tests/unit/docs/test_code_review_docs_parity.py +++ b/tests/unit/docs/test_code_review_docs_parity.py @@ -3,6 +3,7 @@ from __future__ import annotations from pathlib import Path +from typing import cast import click import pytest @@ -28,16 +29,15 @@ def _review_run_click_command() -> click.Command: review_group = typer_get_command(review_commands.review_app) run_cmd = review_group.commands.get("run") - assert isinstance(run_cmd, click.Command) - return run_cmd + assert run_cmd is not None + return cast(click.Command, run_cmd) def _public_option_flags(command: click.Command) -> set[str]: flags: set[str] = set() for param in command.params: - if not isinstance(param, click.Option): - continue - for opt in param.opts: + opts = getattr(param, "opts", ()) + for opt in opts: if opt.startswith("--"): flags.add(opt) return flags @@ -52,7 +52,7 @@ def test_code_review_run_doc_mentions_public_ty_options() -> None: assert "progress" in text assert "spinner" in text or "status" in text - assert "default **`enforce`**" in text + assert "default **`changed`**" in text assert "Optional reporting level override" in text assert "--bug-hunt" in text assert "exploratory" in text.lower() @@ -99,7 +99,7 @@ def test_code_review_run_doc_describes_invalid_flag_combinations() -> None: assert "request validation" in text.lower() assert "conflicting targeting styles" in text.lower() assert "progress" in text - assert "default **`enforce`**" in text + assert "default **`changed`**" in text assert "Optional reporting level override" in text assert "review-report.json" in text diff --git a/tests/unit/scripts/test_pre_commit_code_review.py b/tests/unit/scripts/test_pre_commit_code_review.py index 152cfc48..d3a50285 100644 --- a/tests/unit/scripts/test_pre_commit_code_review.py +++ b/tests/unit/scripts/test_pre_commit_code_review.py @@ -6,6 +6,7 @@ import importlib.util import json +import os import subprocess import sys from pathlib import Path @@ -86,6 +87,8 @@ def test_build_review_command_writes_json_report() -> None: assert command[:5] == [sys.executable, "-m", "specfact_cli.cli", "code", "review"] assert "--json" in command assert "--out" in command + assert "--enforcement" in command + assert "changed" in command assert "--level" not in command assert module.REVIEW_JSON_OUT in command assert command[-2:] == ["tests/test_app.py", "packages/specfact-spec/src/x.py"] @@ -207,6 +210,7 @@ def _fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[ monkeypatch.setattr(module, "_repo_root", lambda: repo_root) monkeypatch.setattr(module, "ensure_runtime_available", lambda: (True, None)) monkeypatch.setattr(module.subprocess, "run", _fake_run) + monkeypatch.setenv("SPECFACT_CODE_REVIEW_ENFORCEMENT", "full") assert module.main(["tests/unit/test_app.py"]) == 1 err = capsys.readouterr().err @@ -241,6 +245,7 @@ def _fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[ monkeypatch.setattr(module, "_repo_root", _fake_root) monkeypatch.setattr(module, "ensure_runtime_available", _fake_ensure) monkeypatch.setattr(module.subprocess, "run", _fake_run) + monkeypatch.setenv("SPECFACT_CODE_REVIEW_ENFORCEMENT", "full") exit_code = module.main(["tests/unit/test_app.py"]) @@ -249,6 +254,7 @@ def _fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[ assert captured.out == "" err = captured.err assert "Code review summary: 2 finding(s)" in err + assert "Code review enforcement: full" in err assert "errors=1" in err assert "warnings=1" in err assert "overall_verdict='FAIL'" in err @@ -275,10 +281,52 @@ def _fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[ monkeypatch.setattr(module, "_repo_root", lambda: repo_root) monkeypatch.setattr(module, "ensure_runtime_available", lambda: (True, None)) monkeypatch.setattr(module.subprocess, "run", _fake_run) + monkeypatch.setenv("SPECFACT_CODE_REVIEW_ENFORCEMENT", "full") assert module.main(["tests/unit/test_app.py"]) == 0 +def test_review_enforcement_mode_defaults_to_changed() -> None: + module = _load_script_module() + + assert module.review_enforcement_mode() == "changed" + + +def test_review_enforcement_mode_rejects_invalid_value( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + module = _load_script_module() + + monkeypatch.setenv("SPECFACT_CODE_REVIEW_ENFORCEMENT", "strict") + + assert module.review_enforcement_mode() == "changed" + assert "Invalid SPECFACT_CODE_REVIEW_ENFORCEMENT" in capsys.readouterr().err + + +def test_changed_enforcement_blocks_findings_on_staged_lines(tmp_path: Path) -> None: + module = _load_script_module() + payload: dict[str, object] = { + "overall_verdict": "FAIL", + "ci_exit_code": 1, + "findings": [ + {"severity": "error", "fixable": False, "file": "tests/unit/test_app.py", "line": 7}, + ], + } + _write_sample_review_report(tmp_path, payload) + + module._staged_changed_lines = lambda _repo_root: {"tests/unit/test_app.py": {7}} + + assert module._print_review_findings_summary(tmp_path, enforcement="changed") == (True, 1, 1) + + +def test_shadow_enforcement_reports_without_blocking(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + module = _load_script_module() + _write_sample_review_report(tmp_path, SAMPLE_FAIL_REVIEW_REPORT) + + assert module._print_review_findings_summary(tmp_path, enforcement="shadow") == (True, 1, 0) + assert "shadow gate" in capsys.readouterr().err + + def _write_sample_review_report(repo_root: Path, payload: dict[str, object]) -> None: spec_dir = repo_root / ".specfact" spec_dir.mkdir(parents=True, exist_ok=True) @@ -354,6 +402,35 @@ def _fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[ assert "tests/unit/test_app.py" in err +def test_run_review_subprocess_exposes_local_module_sources(monkeypatch: pytest.MonkeyPatch) -> None: + """Nested review command must load in-repo modules, not only installed user copies.""" + module = _load_script_module() + repo_root = Path(__file__).resolve().parents[3] + captured_env: dict[str, str] = {} + + def _fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + env = kwargs.get("env") + assert isinstance(env, dict) + captured_env.update(env) + return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="") + + monkeypatch.setattr(module.subprocess, "run", _fake_run) + + result = module._run_review_subprocess( + ["python", "-m", "specfact_cli.cli"], + repo_root, + ["tests/unit/test_app.py"], + enforcement="changed", + ) + + assert result is not None + assert captured_env["SPECFACT_MODULES_ROOTS"] == str((repo_root / "packages").resolve()) + assert captured_env["SPECFACT_CODE_REVIEW_CHANGED_DIFF"] == "cached" + pythonpath = captured_env["PYTHONPATH"].split(os.pathsep) + assert str(repo_root / "packages" / "specfact-codebase" / "src") in pythonpath + assert str(repo_root / "packages" / "specfact-code-review" / "src") in pythonpath + + def test_main_prints_actionable_setup_guidance_when_runtime_missing( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/unit/specfact_backlog/conftest.py b/tests/unit/specfact_backlog/conftest.py index 5fde2daa..9eaadaec 100644 --- a/tests/unit/specfact_backlog/conftest.py +++ b/tests/unit/specfact_backlog/conftest.py @@ -35,8 +35,7 @@ def _patch_clirunner() -> None: # pylint: disable=import-outside-toplevel from collections.abc import Callable - from click.testing import Result - from typer.testing import CliRunner + from typer.testing import CliRunner, Result original_invoke: Callable[..., Result] = CliRunner.invoke diff --git a/tests/unit/specfact_backlog/test_auth_commands.py b/tests/unit/specfact_backlog/test_auth_commands.py index eced6039..b0b0b213 100644 --- a/tests/unit/specfact_backlog/test_auth_commands.py +++ b/tests/unit/specfact_backlog/test_auth_commands.py @@ -64,3 +64,17 @@ def fake_clear_token(provider: str) -> None: assert result.exit_code == 0 assert captured["provider"] == "github" + + +def test_auth_without_subcommand_shows_help_and_missing_subcommand() -> None: + backlog_app = importlib.import_module("specfact_backlog.backlog.commands").app + + result = runner.invoke(backlog_app, ["auth"]) + + assert result.exit_code != 0 + output = result.stdout.lower() + assert "usage:" in output + assert "azure-devops" in output + assert "github" in output + assert "status" in output + assert "subcommand" in output diff --git a/tests/unit/specfact_backlog/test_delta_command_contract.py b/tests/unit/specfact_backlog/test_delta_command_contract.py new file mode 100644 index 00000000..11066217 --- /dev/null +++ b/tests/unit/specfact_backlog/test_delta_command_contract.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import importlib +from pathlib import Path + +from typer.testing import CliRunner + + +runner = CliRunner() + + +def test_delta_command_avoids_private_typer_click_import() -> None: + source = Path("packages/specfact-backlog/src/specfact_backlog/backlog_core/commands/delta.py").read_text( + encoding="utf-8" + ) + + assert "typer._click" not in source + + +def test_delta_status_accepts_adapter_argument_and_configured_project(tmp_path: Path, monkeypatch) -> None: + delta_module = importlib.import_module("specfact_backlog.backlog_core.commands.delta") + backlog_app = importlib.import_module("specfact_backlog.backlog.commands").app + fetched: dict[str, str] = {} + + config_dir = tmp_path / ".specfact" + config_dir.mkdir() + (config_dir / "backlog-config.yaml").write_text( + "providers:\n github:\n project_id: nold-ai/specfact-cli\n repo_owner: nold-ai\n repo_name: specfact-cli\n", + encoding="utf-8", + ) + + class FakeGraph: + fetched_at = delta_module.datetime.now() + items: dict[str, object] = {} + + def fake_fetch_current_graph(project_id: str, adapter: str, template: str): + fetched["project_id"] = project_id + fetched["adapter"] = adapter + fetched["template"] = template + return FakeGraph() + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(delta_module, "_fetch_current_graph", fake_fetch_current_graph) + monkeypatch.setattr(delta_module, "_load_baseline_graph", lambda baseline_file: FakeGraph()) + monkeypatch.setattr(delta_module, "compute_delta", lambda baseline, current: delta_module._empty_delta()) + + result = runner.invoke(backlog_app, ["delta", "status", "github"]) + + assert result.exit_code == 0, result.stdout + assert fetched["adapter"] == "github" + assert fetched["project_id"] == "nold-ai/specfact-cli" + + +def test_delta_status_missing_config_names_kebab_case_options(tmp_path: Path, monkeypatch) -> None: + backlog_app = importlib.import_module("specfact_backlog.backlog.commands").app + monkeypatch.chdir(tmp_path) + + result = runner.invoke(backlog_app, ["delta", "status", "github"]) + + assert result.exit_code != 0 + output = result.stdout + assert "--project-id" in output + assert "--repo-owner" in output + assert "--repo-name" in output + assert "repo_owner" not in output + assert "repo_name" not in output + + +def test_delta_status_malformed_config_preserves_missing_context_guidance(tmp_path: Path, monkeypatch) -> None: + backlog_app = importlib.import_module("specfact_backlog.backlog.commands").app + config_dir = tmp_path / ".specfact" + config_dir.mkdir() + (config_dir / "backlog-config.yaml").write_text("providers:\n github: [unterminated\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + + result = runner.invoke(backlog_app, ["delta", "status", "github"]) + + assert result.exit_code != 0 + output = result.stdout + assert "--project-id" in output + assert "--repo-owner" in output + assert "--repo-name" in output + assert "ParserError" not in output diff --git a/tests/unit/specfact_code_review/review/test_commands.py b/tests/unit/specfact_code_review/review/test_commands.py index 0f0e7268..e0b40ccb 100644 --- a/tests/unit/specfact_code_review/review/test_commands.py +++ b/tests/unit/specfact_code_review/review/test_commands.py @@ -35,19 +35,22 @@ def _fail_run_command(_files: list[Path], **_kwargs: object) -> tuple[int, str | result = runner.invoke(app, ["review", "run", "--instructions"]) assert result.exit_code == 0 - assert "remove AI bloat" in result.output - assert "safe_mechanical" in result.output - assert "design_judgment" in result.output - assert "branch-delta Python files" in result.output - assert "git diff --name-only <base-ref>...HEAD" in result.output - assert "Findings without guidance_kind are unguided advisories" in result.output - assert "Sort findings by guidance_kind before editing" in result.output - assert "exact patch preview" in result.output - assert "default to keep or skip" in result.output - assert "specfact code review run --scope changed --focus simplify" in result.output - assert "cleanup_forecast" in result.output - assert "remediation_packet" in result.output - assert "not proof of AI authorship" in result.output + expected_snippets = ( + "remove AI bloat", + "safe_mechanical", + "design_judgment", + "branch-delta Python files", + "git diff --name-only <base-ref>...HEAD", + "Findings without guidance_kind are unguided advisories", + "Sort findings by guidance_kind before editing", + "exact patch preview", + "default to keep or skip", + "specfact code review run --scope changed --enforcement shadow --focus simplify", + "cleanup_forecast", + "remediation_packet", + "not proof of AI authorship", + ) + assert all(snippet in result.output for snippet in expected_snippets) def test_review_run_interactive_prompts_for_test_inclusion(monkeypatch: Any) -> None: @@ -83,6 +86,43 @@ def _fake_run_command(_files: list[Path], **kwargs: object) -> tuple[int, str | assert recorded["kwargs"]["include_tests"] is False +def test_review_run_warns_when_enforcement_defaults_to_changed(monkeypatch: Any) -> None: + def _fake_run_command(_files: list[Path], **_kwargs: object) -> tuple[int, str | None]: + return 0, None + + monkeypatch.setattr("specfact_code_review.review.commands.run_command", _fake_run_command) + + result = runner.invoke(app, ["review", "run"]) + + assert result.exit_code == 0 + assert "Code review enforcement default is 'changed'" in result.output + assert "--enforcement full" in result.output + + +def test_review_run_explicit_changed_enforcement_does_not_warn(monkeypatch: Any) -> None: + def _fake_run_command(_files: list[Path], **_kwargs: object) -> tuple[int, str | None]: + return 0, None + + monkeypatch.setattr("specfact_code_review.review.commands.run_command", _fake_run_command) + + result = runner.invoke(app, ["review", "run", "--enforcement", "changed"]) + + assert result.exit_code == 0 + assert "Code review enforcement default is 'changed'" not in result.output + + +def test_review_run_rejects_legacy_mode_with_explicit_enforcement(monkeypatch: Any) -> None: + def _fail_run_command(_files: list[Path], **_kwargs: object) -> tuple[int, str | None]: + raise AssertionError("run_command should not be called with ambiguous enforcement flags") + + monkeypatch.setattr("specfact_code_review.review.commands.run_command", _fail_run_command) + + result = runner.invoke(app, ["review", "run", "--mode", "shadow", "--enforcement", "changed"]) + + assert result.exit_code != 0 + assert "Use only one of --mode or --enforcement" in _plain_output(result.output) + + def test_review_run_focus_source_sets_include_tests_false(monkeypatch: Any) -> None: recorded: dict[str, Any] = {} diff --git a/tests/unit/specfact_code_review/run/test_commands.py b/tests/unit/specfact_code_review/run/test_commands.py index 3858d01f..b5672ede 100644 --- a/tests/unit/specfact_code_review/run/test_commands.py +++ b/tests/unit/specfact_code_review/run/test_commands.py @@ -289,6 +289,59 @@ def fake_run_review(files: list[Path], **kwargs: Any) -> ReviewReport: assert recorded == {"files": [package_file], "focus": "simplify"} +def test_run_command_passes_enforcement_mode_to_review_runtime(monkeypatch: Any, tmp_path: Path) -> None: + package_file = _write_repo_file( + tmp_path, + "packages/specfact-code-review/src/specfact_code_review/run/commands.py", + ) + monkeypatch.chdir(tmp_path) + recorded: dict[str, object] = {} + + def fake_run_review(files: list[Path], **kwargs: Any) -> ReviewReport: + recorded["files"] = files + recorded["review_mode"] = kwargs.get("review_mode") + return _report() + + monkeypatch.setattr("specfact_code_review.run.commands.run_review", fake_run_review) + + result = runner.invoke( + app, + [ + "review", + "run", + "--enforcement", + "shadow", + "--json", + "--out", + "review-report.json", + str(package_file), + ], + ) + + assert result.exit_code == 0 + assert recorded == {"files": [package_file], "review_mode": "shadow"} + + +def test_run_command_maps_legacy_enforce_mode_to_full_enforcement(monkeypatch: Any, tmp_path: Path) -> None: + package_file = _write_repo_file( + tmp_path, + "packages/specfact-code-review/src/specfact_code_review/run/commands.py", + ) + monkeypatch.chdir(tmp_path) + recorded: dict[str, object] = {} + + def fake_run_review(files: list[Path], **kwargs: Any) -> ReviewReport: + recorded["review_mode"] = kwargs.get("review_mode") + return _report() + + monkeypatch.setattr("specfact_code_review.run.commands.run_review", fake_run_review) + + result = runner.invoke(app, ["review", "run", "--mode", "enforce", "--json", str(package_file)]) + + assert result.exit_code == 0 + assert recorded == {"review_mode": "full"} + + def test_run_command_normalizes_simplify_focus_on_direct_request(monkeypatch: Any, tmp_path: Path) -> None: package_file = _write_repo_file( tmp_path, @@ -371,6 +424,7 @@ def test_preview_fixes_adds_patch_forecast_without_mutating_tracked_file(monkeyp out=tmp_path / "review-report.json", focus_facets=("simplify",), preview_fixes=True, + review_mode="full", ) ) @@ -400,6 +454,7 @@ def test_with_mutation_records_inconclusive_evidence_for_missing_tool(monkeypatc out=tmp_path / "review-report.json", focus_facets=("simplify",), with_mutation=True, + review_mode="full", ) ) @@ -688,7 +743,7 @@ def test_run_review_once_applies_simplification_fixes_before_rerun(monkeypatch: with_mutation=False, progress_callback=None, bug_hunt=False, - review_mode="enforce", + review_mode="full", review_level=None, review_focus="simplify", ), diff --git a/tests/unit/specfact_code_review/run/test_runner.py b/tests/unit/specfact_code_review/run/test_runner.py index 4c6bdc5a..bba8d84c 100644 --- a/tests/unit/specfact_code_review/run/test_runner.py +++ b/tests/unit/specfact_code_review/run/test_runner.py @@ -12,6 +12,7 @@ from specfact_code_review.run.findings import ReviewFinding, ReviewReport from specfact_code_review.run.runner import ( + _changed_lines_from_git, _coverage_findings, _preserve_reasons_for_finding, _pytest_python_executable, @@ -93,6 +94,72 @@ def _simplification_finding( ) +def _stub_review_tools(monkeypatch: MonkeyPatch, findings: list[ReviewFinding]) -> None: + monkeypatch.setattr("specfact_code_review.run.runner.run_ruff", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_radon", lambda files: findings) + monkeypatch.setattr("specfact_code_review.run.runner.run_semgrep", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_semgrep_bugs", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_ai_bloat", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_ast_clean_code", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_basedpyright", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_pylint", lambda files: []) + monkeypatch.setattr("specfact_code_review.run.runner.run_contract_check", lambda files, **_: []) + monkeypatch.setattr("specfact_code_review.run.runner._evaluate_tdd_gate", lambda files: ([], None)) + + +def test_run_review_changed_enforcement_reports_legacy_blockers_without_blocking(monkeypatch: MonkeyPatch) -> None: + finding = _finding(tool="radon", rule="complexity", severity="error", category="kiss") + _stub_review_tools(monkeypatch, [finding]) + monkeypatch.setattr("specfact_code_review.run.runner._changed_lines_from_git", lambda files: {finding.file: {99}}) + + report = run_review([Path(finding.file)], no_tests=True, review_mode="changed") + + assert report.ci_exit_code == 0 + assert report.overall_verdict == "PASS_WITH_ADVISORY" + assert report.enforcement_mode == "changed" + assert "legacy blocking" in (report.enforcement_summary or "") + + +def test_run_review_changed_enforcement_blocks_changed_line_findings(monkeypatch: MonkeyPatch) -> None: + finding = _finding(tool="radon", rule="complexity", severity="error", category="kiss") + _stub_review_tools(monkeypatch, [finding]) + monkeypatch.setattr("specfact_code_review.run.runner._changed_lines_from_git", lambda files: {finding.file: {10}}) + + report = run_review([Path(finding.file)], no_tests=True, review_mode="changed") + + assert report.ci_exit_code == 1 + assert report.overall_verdict == "FAIL" + assert report.enforcement_mode == "changed" + assert "changed lines" in (report.enforcement_summary or "") + + +def test_run_review_shadow_enforcement_never_blocks(monkeypatch: MonkeyPatch) -> None: + finding = _finding(tool="radon", rule="complexity", severity="error", category="kiss") + _stub_review_tools(monkeypatch, [finding]) + + report = run_review([Path(finding.file)], no_tests=True, review_mode="shadow") + + assert report.ci_exit_code == 0 + assert report.overall_verdict == "FAIL" + assert report.enforcement_mode == "shadow" + + +def test_changed_lines_from_git_skips_unreadable_untracked_files(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: + untracked_file = tmp_path / "binary.py" + untracked_file.write_bytes(b"\xff\xfe") + + def _fake_run(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + if command[:3] == ["git", "diff", "--unified=0"]: + return subprocess.CompletedProcess(command, 0, stdout="", stderr="") + if command[:3] == ["git", "ls-files", "--others"]: + return subprocess.CompletedProcess(command, 0, stdout=f"{untracked_file}\n", stderr="") + raise AssertionError(f"unexpected command: {command}") + + monkeypatch.setattr(subprocess, "run", _fake_run) + + assert _changed_lines_from_git([untracked_file]) == {} + + def test_run_review_calls_runners_in_order(monkeypatch: MonkeyPatch) -> None: calls: list[str] = [] diff --git a/tests/unit/specfact_codebase/test_import_command_contract.py b/tests/unit/specfact_codebase/test_import_command_contract.py new file mode 100644 index 00000000..c2bf3787 --- /dev/null +++ b/tests/unit/specfact_codebase/test_import_command_contract.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typer.testing import CliRunner + +from specfact_codebase.import_cmd.commands import app + + +def test_code_import_legacy_option_order_reports_canonical_invocation(tmp_path) -> None: + result = CliRunner().invoke(app, ["legacy-api", "--repo", str(tmp_path)]) + + assert result.exit_code != 0 + output = result.stdout.lower() + assert "canonical" in output or "use:" in output + assert "code import --repo" in output + assert "no such command '--repo'" not in output diff --git a/tests/unit/specfact_project/test_code_analyzer_semgrep_status.py b/tests/unit/specfact_project/test_code_analyzer_semgrep_status.py new file mode 100644 index 00000000..45ccf955 --- /dev/null +++ b/tests/unit/specfact_project/test_code_analyzer_semgrep_status.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from pathlib import Path + +from specfact_cli.utils.env_manager import EnvManager, EnvManagerInfo + +from specfact_project.analyzers.code_analyzer import CodeAnalyzer + + +def test_semgrep_plugin_status_preserves_environment_probe_message(tmp_path: Path, monkeypatch) -> None: + message = "Tool 'semgrep' not available in uv environment" + + monkeypatch.delenv("TEST_MODE", raising=False) + monkeypatch.setattr( + "specfact_cli.utils.env_manager.detect_env_manager", + lambda repo_path: EnvManagerInfo(manager=EnvManager.UV, available=True, command_prefix=["uv", "run"]), + ) + monkeypatch.setattr( + "specfact_cli.utils.env_manager.check_tool_in_env", + lambda repo_path, tool_name, env_info=None: (False, message), + ) + + analyzer = CodeAnalyzer(tmp_path) + semgrep_status = next( + plugin for plugin in analyzer.get_plugin_status() if plugin["name"] == "Semgrep Pattern Detection" + ) + + assert semgrep_status["enabled"] is False + assert semgrep_status["reason"] == message diff --git a/tests/unit/specfact_project/test_regenerate_command_contract.py b/tests/unit/specfact_project/test_regenerate_command_contract.py new file mode 100644 index 00000000..821d83d9 --- /dev/null +++ b/tests/unit/specfact_project/test_regenerate_command_contract.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from typer.testing import CliRunner + +from specfact_project.project import commands + + +runner = CliRunner() + + +class _ProjectMetadata: + def get_extension(self, namespace: str, key: str) -> dict[str, str] | None: + if namespace == "backlog_core" and key == "backlog_config": + return {"adapter": "github", "project_id": "owner/repo", "template": "github_projects"} + return None + + +@dataclass +class _Manifest: + project_metadata: _ProjectMetadata + + +@dataclass +class _Bundle: + manifest: _Manifest + features: list[str] + + +def test_project_regenerate_reports_typed_null_backlog_graph( + monkeypatch, + tmp_path: Path, +) -> None: + """Null backlog graph data should produce a typed diagnostic, not a raw NoneType crash.""" + monkeypatch.setattr( + commands, "_resolve_bundle", lambda repo, bundle: ("demo", tmp_path / ".specfact/projects/demo") + ) + monkeypatch.setattr( + commands, + "_load_bundle_with_progress", + lambda bundle_dir, validate_hashes=False: _Bundle(_Manifest(_ProjectMetadata()), ["FEATURE-1"]), + ) + monkeypatch.setattr( + commands, "_resolve_linked_backlog_config", lambda bundle_obj: ("github", "owner/repo", "github_projects") + ) + monkeypatch.setattr(commands, "_fetch_backlog_graph", lambda **kwargs: None) + + result = runner.invoke(commands.app, ["regenerate", "--repo", str(tmp_path), "--bundle", "demo"]) + + assert result.exit_code == 1 + assert "Backlog graph data unavailable" in result.stdout + assert "specfact backlog analyze-deps" in result.stdout + assert "NoneType" not in result.stdout + assert not isinstance(result.exception, AttributeError) + + +def test_project_snapshot_reports_typed_null_backlog_graph( + monkeypatch, + tmp_path: Path, +) -> None: + """Snapshot should share the backlog graph guard instead of dereferencing None.""" + monkeypatch.setattr( + commands, "_resolve_bundle", lambda repo, bundle: ("demo", tmp_path / ".specfact/projects/demo") + ) + monkeypatch.setattr( + commands, + "_load_bundle_with_progress", + lambda bundle_dir, validate_hashes=False: _Bundle(_Manifest(_ProjectMetadata()), ["FEATURE-1"]), + ) + monkeypatch.setattr( + commands, "_resolve_linked_backlog_config", lambda bundle_obj: ("github", "owner/repo", "github_projects") + ) + monkeypatch.setattr(commands, "_fetch_backlog_graph", lambda **kwargs: None) + + result = runner.invoke(commands.app, ["snapshot", "--repo", str(tmp_path), "--bundle", "demo"]) + + assert result.exit_code == 1 + assert "Backlog graph data unavailable" in result.stdout + assert "specfact backlog analyze-deps" in result.stdout + assert "NoneType" not in result.stdout + assert not isinstance(result.exception, AttributeError) diff --git a/tests/unit/test_check_docs_commands_script.py b/tests/unit/test_check_docs_commands_script.py index 3d97398b..bdf849d2 100644 --- a/tests/unit/test_check_docs_commands_script.py +++ b/tests/unit/test_check_docs_commands_script.py @@ -2,6 +2,8 @@ from pathlib import Path +import pytest + from tests.unit._script_test_utils import load_module_from_path @@ -17,6 +19,14 @@ def _script_attr(script, name: str): return getattr(script, name) +def test_docs_command_mounts_include_nested_prompt_validator_mounts() -> None: + script = _load_script() + mounts = set(_script_attr(script, "MODULE_APP_MOUNTS")) + + assert ("specfact_govern.enforce.commands", "app", ("specfact", "govern", "enforce")) in mounts + assert ("specfact_spec.contract.commands", "app", ("specfact", "spec", "contract")) in mounts + + def test_extract_command_examples_reads_bash_and_inline_examples(tmp_path: Path) -> None: script = _load_script() doc_path = tmp_path / "example.md" @@ -86,6 +96,26 @@ def test_command_example_is_valid_allows_root_help_but_not_unknown_subgroups() - assert not _script_attr(script, "_command_example_is_valid")("specfact policy validate --repo .", valid_paths) +def test_build_valid_command_paths_rejects_malformed_generated_json(tmp_path: Path, monkeypatch) -> None: + script = _load_script() + generated = tmp_path / "commands.generated.json" + generated.write_text('{"command": "specfact backlog"}\n', encoding="utf-8") + monkeypatch.setattr(script, "GENERATED_COMMANDS_PATH", generated) + + with pytest.raises(ValueError, match="expected a JSON list"): + _script_attr(script, "_build_valid_command_paths")() + + +def test_build_valid_command_paths_rejects_malformed_generated_entries(tmp_path: Path, monkeypatch) -> None: + script = _load_script() + generated = tmp_path / "commands.generated.json" + generated.write_text('[{"owner_package": "specfact-backlog"}]\n', encoding="utf-8") + monkeypatch.setattr(script, "GENERATED_COMMANDS_PATH", generated) + + with pytest.raises(ValueError, match="missing 'command'"): + _script_attr(script, "_build_valid_command_paths")() + + def test_validate_legacy_resource_paths_reports_stale_core_owned_paths(tmp_path: Path) -> None: script = _load_script() doc_path = tmp_path / "legacy.md" @@ -163,6 +193,19 @@ def test_docs_review_workflow_runs_docs_command_validation() -> None: assert "tests/unit/docs/test_code_review_docs_parity.py" in workflow +def test_docs_review_workflow_uses_matching_core_branch_when_available() -> None: + workflow = (REPO_ROOT / ".github" / "workflows" / "docs-review.yml").read_text(encoding="utf-8") + + assert "id: core-ref" in workflow + assert "git ls-remote --exit-code --heads https://github.com/nold-ai/specfact-cli.git" in workflow + assert "FALLBACK_REF: ${{ github.base_ref || github.ref_name }}" in workflow + assert 'echo "ref=$fallback" >> "$GITHUB_OUTPUT"' in workflow + assert "ref: ${{ steps.core-ref.outputs.ref }}" in workflow + assert ( + "ref: ${{ (github.ref == 'refs/heads/main' || github.head_ref == 'main') && 'main' || 'dev' }}" not in workflow + ) + + def test_iter_validation_docs_paths_scans_repo_wide_docs_tree() -> None: script = _load_script() diff --git a/tests/unit/test_check_prompt_commands_script.py b/tests/unit/test_check_prompt_commands_script.py index 897a744d..5e2a4784 100644 --- a/tests/unit/test_check_prompt_commands_script.py +++ b/tests/unit/test_check_prompt_commands_script.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from types import SimpleNamespace import pytest @@ -116,6 +117,13 @@ def test_validate_prompt_commands_reports_stale_nested_subcommand_path(tmp_path: assert "specfact code stale-subcmd --repo ." in findings[0].message +def test_command_options_tolerates_params_without_secondary_opts() -> None: + script = _load_script() + command = SimpleNamespace(params=[SimpleNamespace(opts=["--repo"])]) + + assert _script_attr(script, "_command_options")(command) == {"--repo"} + + def test_main_writes_findings_to_stderr(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: script = _load_script() prompt = _write_prompt( @@ -163,6 +171,49 @@ def test_validate_prompt_commands_reports_stale_option(tmp_path: Path) -> None: assert "--missing-option" in findings[0].message +def test_iter_prompt_paths_includes_resource_templates(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt(tmp_path, "# Prompt\n") + template = tmp_path / "packages" / "specfact-project" / "resources" / "templates" / "protocol.yaml.j2" + template.parent.mkdir(parents=True) + template.write_text('command: "specfact project sync bridge --help"\n', encoding="utf-8") + + paths = _script_attr(script, "_iter_prompt_paths")(tmp_path / "packages") + + assert prompt.resolve() in paths + assert template.resolve() in paths + + +def test_selected_paths_accepts_changed_python_command_sources(tmp_path: Path) -> None: + script = _load_script() + command_source = tmp_path / "packages" / "specfact-codebase" / "src" / "specfact_codebase" / "repro" / "commands.py" + command_source.parent.mkdir(parents=True) + command_source.write_text('HELP = "specfact code repro --help"\n', encoding="utf-8") + args = script._parse_args([str(command_source)]) + + assert _script_attr(script, "_selected_paths")(args) == [command_source.resolve()] + + +def test_validate_prompt_commands_reports_stale_command_in_resource_template(tmp_path: Path) -> None: + script = _load_script() + template = tmp_path / "packages" / "specfact-project" / "resources" / "templates" / "protocol.yaml.j2" + template.parent.mkdir(parents=True) + template.write_text('command: "specfact sync bridge --help"\n', encoding="utf-8") + command_index = _script_attr(script, "CommandIndex")( + command_paths={("specfact",), ("specfact", "project"), ("specfact", "project", "sync")}, + options_by_path={("specfact", "project", "sync"): {"--help"}}, + ) + + findings = _script_attr(script, "_validate_prompt_command_examples")( + {template: template.read_text(encoding="utf-8")}, + command_index, + ) + + assert len(findings) == 1 + assert findings[0].category == "command" + assert "specfact sync bridge --help" in findings[0].message + + def test_validate_prompt_guidance_requires_cli_reality_check(tmp_path: Path) -> None: script = _load_script() prompt = _write_prompt( @@ -228,10 +279,33 @@ def test_module_app_mounts_include_govern_enforce_app() -> None: ) in _script_attr(script, "MODULE_APP_MOUNTS") +def test_module_app_mounts_do_not_include_removed_flat_shims() -> None: + script = _load_script() + + prefixes = {mount[2] for mount in _script_attr(script, "MODULE_APP_MOUNTS")} + + assert ("specfact", "sync") not in prefixes + assert ("specfact", "import") not in prefixes + assert ("specfact", "plan") not in prefixes + assert ("specfact", "migrate") not in prefixes + + +def test_build_command_index_descends_into_typer_groups() -> None: + script = _load_script() + + index = script._build_command_index() + + assert ("specfact", "backlog", "add") in index.command_paths + assert ("specfact", "project", "sync", "bridge") in index.command_paths + assert ("specfact", "code", "import") in index.command_paths + assert "--repo" in index.options_by_path[("specfact", "code", "import")] + assert "--adapter" in index.options_by_path[("specfact", "project", "sync", "bridge")] + + def test_docs_review_workflow_runs_prompt_command_validation() -> None: workflow = DOCS_REVIEW_WORKFLOW.read_text(encoding="utf-8") - assert "packages/*/resources/prompts/**" in workflow + assert "packages/*/resources/**" in workflow assert "python scripts/check-prompt-commands.py" in workflow assert "scripts/check-prompt-commands.py" in workflow assert "tests/unit/test_check_prompt_commands_script.py" in workflow @@ -241,6 +315,7 @@ def test_pre_commit_runs_prompt_validation_before_safe_change_skip() -> None: script = PRE_COMMIT_SCRIPT.read_text(encoding="utf-8") assert "check-prompt-commands.py" in script + assert 'hatch run python scripts/check-prompt-commands.py "${validation_paths[@]}"' in script validation_index = script.index("run_prompt_command_validation_gate") safe_change_index = script.index("if check_safe_change; then") assert validation_index < safe_change_index @@ -265,7 +340,7 @@ def test_project_review_prompt_has_self_healing_cli_and_verification_guidance() assert "Guidance Character" in prompt assert "self-heal command drift" in prompt - assert "specfact plan review --help" in prompt + assert "specfact project --help" in prompt assert "Do not write `.specfact/` artifacts directly" in prompt assert "hatch run validate-prompt-commands" in prompt assert "hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump" in prompt @@ -274,4 +349,5 @@ def test_project_review_prompt_has_self_healing_cli_and_verification_guidance() def test_pre_commit_prompt_validation_covers_cli_command_implementations() -> None: script = PRE_COMMIT_SCRIPT.read_text(encoding="utf-8") + assert "packages/*/resources/**" in script assert "packages/*/src/**/commands.py" in script diff --git a/tests/unit/test_global_cli_error_contract.py b/tests/unit/test_global_cli_error_contract.py new file mode 100644 index 00000000..bfcdba68 --- /dev/null +++ b/tests/unit/test_global_cli_error_contract.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import importlib + +import typer +from typer.testing import CliRunner + + +runner = CliRunner() + + +def test_module_group_without_subcommand_uses_shared_missing_subcommand_contract() -> None: + app = importlib.import_module("specfact_codebase.code.commands").app + + result = runner.invoke(app, []) + + assert result.exit_code == 2 + output = result.stdout.lower() + assert "usage:" in output + assert "codebase quality" in output + assert "import" in output + assert "analyze" in output + assert "missing subcommand" in output + + +def test_module_leaf_missing_argument_uses_shared_missing_parameter_contract() -> None: + app = typer.Typer(name="module-sample") + + def apply_module_change(change_id: str) -> None: + typer.echo(change_id) + + def list_module_changes() -> None: + typer.echo("[]") + + app.command("apply")(apply_module_change) + app.command("list")(list_module_changes) + + result = runner.invoke(app, ["apply"]) + + assert result.exit_code == 2 + output = result.stdout.lower() + assert "usage:" in output + assert "apply" in output + assert "missing" in output + assert "change-id" in output or "change_id" in output diff --git a/tests/unit/test_pre_commit_quality_parity.py b/tests/unit/test_pre_commit_quality_parity.py index cdc3584b..9bfb5a76 100644 --- a/tests/unit/test_pre_commit_quality_parity.py +++ b/tests/unit/test_pre_commit_quality_parity.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.util import itertools from pathlib import Path @@ -37,7 +38,11 @@ "run_block2", "run_docs_site_validation_gate", "hatch run python scripts/check-docs-commands.py", + "SPECFACT_CODE_REVIEW_ENFORCEMENT", + "enforcement=${enforcement}", "needs_docs_site_validation", + "Command overview inputs have unstaged changes", + "git diff --name-only -- packages scripts/generate-command-overview.py scripts/check-command-contract.py pyproject.toml", "usage_error", "show_help", "also: -h | --help | help", @@ -49,6 +54,18 @@ def _load_pre_commit_config() -> dict[str, object]: return loaded if isinstance(loaded, dict) else {} +def _load_pre_commit_code_review_module() -> object: + spec = importlib.util.spec_from_file_location( + "pre_commit_code_review_for_tests", + REPO_ROOT / "scripts" / "pre_commit_code_review.py", + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def _collect_ordered_hook_ids(repos: object) -> tuple[set[str], list[str]]: if not isinstance(repos, list): return set(), [] @@ -112,3 +129,56 @@ def test_modules_pre_commit_script_enforces_required_quality_commands() -> None: script_text = script_path.read_text(encoding="utf-8") for fragment in _REQUIRED_SCRIPT_FRAGMENTS: assert fragment in script_text + + +def test_code_review_gate_parses_staged_added_lines() -> None: + review_gate = _load_pre_commit_code_review_module() + diff_text = """\ +diff --git a/pkg/example.py b/pkg/example.py +index 1111111..2222222 100644 +--- a/pkg/example.py ++++ b/pkg/example.py +@@ -9,0 +10,2 @@ ++added_one() ++added_two() +@@ -20 +23 @@ +-old() ++new() +""" + + changed_lines = review_gate._parse_added_lines_from_cached_diff(diff_text) + + assert changed_lines == {"pkg/example.py": {10, 11, 23}} + + +def test_code_review_gate_does_not_treat_added_content_as_diff_header() -> None: + review_gate = _load_pre_commit_code_review_module() + diff_text = """\ +diff --git a/pkg/example.py b/pkg/example.py +index 1111111..2222222 100644 +--- a/pkg/example.py ++++ b/pkg/example.py +@@ -9,0 +10,2 @@ ++++ not a file header ++added_two() +""" + + changed_lines = review_gate._parse_added_lines_from_cached_diff(diff_text) + + assert changed_lines == {"pkg/example.py": {10, 11}} + + +def test_code_review_gate_blocks_only_findings_on_staged_lines() -> None: + review_gate = _load_pre_commit_code_review_module() + changed_lines = {"pkg/example.py": {10, 11}} + + assert review_gate._finding_targets_staged_line( + REPO_ROOT, + {"file": "pkg/example.py", "line": 10}, + changed_lines, + ) + assert not review_gate._finding_targets_staged_line( + REPO_ROOT, + {"file": "pkg/example.py", "line": 9}, + changed_lines, + ) diff --git a/tests/unit/workflows/test_pr_orchestrator_signing.py b/tests/unit/workflows/test_pr_orchestrator_signing.py index ad9a2852..87c10d07 100644 --- a/tests/unit/workflows/test_pr_orchestrator_signing.py +++ b/tests/unit/workflows/test_pr_orchestrator_signing.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from pathlib import Path @@ -36,7 +37,27 @@ def test_pr_orchestrator_push_uses_github_event_before_for_version_base() -> Non def test_pr_orchestrator_installs_pinned_specfact_cli() -> None: workflow = _workflow_text() - assert 'hatch run pip install "specfact-cli==0.46.2"' in workflow + assert "actions/checkout@v4" in workflow + assert "repository: nold-ai/specfact-cli" in workflow + assert "id: core-ref" in workflow + assert "git ls-remote --exit-code --heads https://github.com/nold-ai/specfact-cli.git" in workflow + assert "FALLBACK_REF: ${{ github.base_ref || github.ref_name }}" in workflow + assert 'echo "ref=$fallback" >> "$GITHUB_OUTPUT"' in workflow + assert "ref: ${{ steps.core-ref.outputs.ref }}" in workflow + assert "ref: dev" not in workflow + assert "hatch run pip install -e ./specfact-cli" in workflow + assert "hatch run python specfact-cli/scripts/runtime_discovery_smoke.py" in workflow + + +def test_pr_orchestrator_has_single_full_pytest_owner() -> None: + workflow = _workflow_text() + assert "hatch run contract-test-contracts" in workflow + assert "hatch run smart-test-check" in workflow + assert "hatch run test" in workflow + assert "hatch run contract-test\n" not in workflow + assert "hatch run smart-test\n" not in workflow + full_suite_runs = re.findall(r"run:\s+hatch run (test|smart-test(?:-full)?|contract-test)(?!-)\b", workflow) + assert full_suite_runs == ["test"] def test_pr_orchestrator_verify_require_signature_on_main_paths() -> None: