diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b32c2a26..3abbf418 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -11,12 +11,12 @@ language: "en-US" early_access: false tone_instructions: >- - Prioritize adapter boundaries between bundled modules and specfact_cli core: registry, - module-package.yaml, signing, and docs parity with modules.specfact.io. Flag cross-repo impact when - core APIs or contracts change. + Focus on bundle/adapter correctness, module signing, docs parity, and specfact_cli impact. + Comment only on correctness, security, regressions, data loss, release integrity, or major + maintainability risks. reviews: - profile: assertive + profile: chill request_changes_workflow: false high_level_summary: true high_level_summary_in_walkthrough: true @@ -42,8 +42,10 @@ reviews: enabled: true drafts: false auto_incremental_review: true + # PRs targeting `dev` (not only the GitHub default branch, e.g. `main`) get automatic reviews. base_branches: - "^dev$" + - "^main$" path_instructions: - path: "packages/**/src/**/*.py" instructions: | diff --git a/.github/workflows/docs-review.yml b/.github/workflows/docs-review.yml index b23bbca4..7c6f9cbc 100644 --- a/.github/workflows/docs-review.yml +++ b/.github/workflows/docs-review.yml @@ -9,10 +9,13 @@ on: - "**/*.md" - "**/*.mdc" - "docs/**" + - "packages/*/resources/prompts/**" - "requirements-docs-ci.txt" - "scripts/check-docs-commands.py" + - "scripts/check-prompt-commands.py" - "scripts/docs_site_validation.py" - "tests/unit/test_check_docs_commands_script.py" + - "tests/unit/test_check_prompt_commands_script.py" - "tests/unit/docs/test_docs_review.py" - "tests/unit/docs/test_code_review_docs_parity.py" - ".github/workflows/docs-review.yml" @@ -22,10 +25,13 @@ on: - "**/*.md" - "**/*.mdc" - "docs/**" + - "packages/*/resources/prompts/**" - "requirements-docs-ci.txt" - "scripts/check-docs-commands.py" + - "scripts/check-prompt-commands.py" - "scripts/docs_site_validation.py" - "tests/unit/test_check_docs_commands_script.py" + - "tests/unit/test_check_prompt_commands_script.py" - "tests/unit/docs/test_docs_review.py" - "tests/unit/docs/test_code_review_docs_parity.py" - ".github/workflows/docs-review.yml" @@ -70,6 +76,13 @@ jobs: python scripts/check-docs-commands.py 2>&1 | tee "$DOCS_COMMAND_LOG" exit "${PIPESTATUS[0]:-$?}" + - name: Validate prompt commands + run: | + 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" + exit "${PIPESTATUS[0]:-$?}" + - name: Upload docs review logs if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/publish-modules.yml b/.github/workflows/publish-modules.yml index 9b8ff5ca..ef7aa56d 100644 --- a/.github/workflows/publish-modules.yml +++ b/.github/workflows/publish-modules.yml @@ -232,7 +232,24 @@ jobs: check=True, ) - skipped_bundles: list[str] = [] + def write_registry_signature_sidecar(bundle: str, manifest: dict, *, expected_version: str | None = None) -> None: + version = str(manifest.get("version") or "").strip() + if expected_version is not None and version != expected_version: + print( + f"::warning::Skipping signature sidecar for {bundle}: " + f"manifest version {version or ''} does not match published version {expected_version}.", + flush=True, + ) + return + integrity = manifest.get("integrity") + signature_text = "" + if isinstance(integrity, dict): + signature_text = str(integrity.get("signature") or "").strip() + if version and signature_text: + signature_path = registry_signatures_dir / f"{bundle}-{version}.tar.sig" + signature_path.write_text(signature_text + "\n", encoding="utf-8") + + skipped_bundles: dict[str, str] = {} current_branch = os.environ.get("GITHUB_REF_NAME", "").strip() baseline_ref = determine_registry_baseline_ref( current_branch=current_branch, @@ -250,6 +267,18 @@ jobs: ) baseline_index_path.write_text(raw_baseline, encoding="utf-8") + baseline_registry = json.loads(baseline_index_path.read_text(encoding="utf-8")) + baseline_modules = baseline_registry.get("modules") + if not isinstance(baseline_modules, list): + raise ValueError("baseline registry/index.json must contain a 'modules' list") + + def registry_latest_version(bundle: str, manifest: dict | None = None) -> str: + module_id = str((manifest or {}).get("name") or f"nold-ai/{bundle}").strip() + for item in baseline_modules: + if isinstance(item, dict) and str(item.get("id") or "").strip() == module_id: + return str(item.get("latest_version") or "").strip() + return "" + for bundle in bundles: reasons = bundle_reasons.get(bundle, []) print(f"Processing {bundle} because of: {', '.join(reasons) if reasons else 'unspecified'}") @@ -277,7 +306,11 @@ jobs: print(f"Skipping {bundle}: registry already at manifest version.") if combined_output: print(combined_output) - skipped_bundles.append(bundle) + manifest_path = repo_root / "packages" / bundle / "module-package.yaml" + manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + if not isinstance(manifest, dict): + raise ValueError(f"Invalid manifest content: {manifest_path}") + skipped_bundles[bundle] = registry_latest_version(bundle, manifest) continue if combined_output: print(combined_output) @@ -321,7 +354,6 @@ jobs: artifact_name = f"{bundle}-{version}.tar.gz" artifact_path = registry_modules_dir / artifact_name - signature_path = registry_signatures_dir / f"{bundle}-{version}.tar.sig" with tarfile.open(artifact_path, mode="w:gz") as tar: for path in sorted(bundle_dir.rglob("*")): @@ -337,11 +369,7 @@ jobs: digest = hashlib.sha256(artifact_path.read_bytes()).hexdigest() (artifact_path.with_suffix(artifact_path.suffix + ".sha256")).write_text(f"{digest}\n", encoding="utf-8") - integrity = manifest.get("integrity") - if isinstance(integrity, dict): - signature_text = str(integrity.get("signature") or "").strip() - if signature_text: - signature_path.write_text(signature_text + "\n", encoding="utf-8") + write_registry_signature_sidecar(bundle, manifest) entry = next( ( @@ -370,13 +398,17 @@ jobs: print(f"Published registry artifact for {module_id} v{version}") if skipped_bundles: - print(f"Skipped already-published bundles: {skipped_bundles}") + print(f"Skipped already-published bundles: {sorted(skipped_bundles)}") - for bundle in skipped_bundles: + for bundle, published_version in skipped_bundles.items(): + manifest_path = repo_root / "packages" / bundle / "module-package.yaml" sign_manifest_if_unsigned( - repo_root / "packages" / bundle / "module-package.yaml", + manifest_path, reason="registry version already published; still align git manifest signature", ) + manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + if isinstance(manifest, dict): + write_registry_signature_sidecar(bundle, manifest, expected_version=published_version) for manifest_path in sorted((repo_root / "packages").glob("*/module-package.yaml")): sign_manifest_if_unsigned( diff --git a/docs/agent-rules/10-session-bootstrap.md b/docs/agent-rules/10-session-bootstrap.md index 28fcaad0..700b44eb 100644 --- a/docs/agent-rules/10-session-bootstrap.md +++ b/docs/agent-rules/10-session-bootstrap.md @@ -42,6 +42,13 @@ depends_on: 6. If the cache is missing or stale, refresh it with `python scripts/sync_github_hierarchy_cache.py`. 7. Load the additional rule files required by the task signal from the index. +## Hatch environment bootstrap + +- In a fresh worktree, run the first Hatch command serially and wait for it to complete before starting any other `hatch run ...` process. +- Do not launch parallel Hatch commands until the worktree environment has successfully run `hatch run python -m pip --version` or another Hatch command that proves the environment is fully created. +- If Hatch reports missing `pip._internal` modules or other partial `pip` imports, treat the local `.venv` as corrupted: remove the Hatch environment, recreate it with one serial Hatch command, and retry validation only after `pip` is healthy. +- Do not interpret Hatch environment-creation failures as code failures until the local environment has been recreated serially. + ## Stop and continue behavior - If the session is on the main checkout and the user did not override, stop implementation and create or switch to a worktree. diff --git a/docs/agent-rules/50-quality-gates-and-review.md b/docs/agent-rules/50-quality-gates-and-review.md index 38c2cc35..d5fbc0f9 100644 --- a/docs/agent-rules/50-quality-gates-and-review.md +++ b/docs/agent-rules/50-quality-gates-and-review.md @@ -38,6 +38,8 @@ depends_on: ## Quality gate order +Before running quality gates in a fresh worktree, bootstrap the Hatch environment serially. Run one Hatch command first, wait for it to finish, and only then run additional gates. Parallel `hatch run ...` commands are allowed only after the worktree environment is known healthy; otherwise concurrent environment creation can corrupt the local `.venv` and leave partial `pip._internal` imports. + 1. `hatch run format` 2. `hatch run type-check` 3. `hatch run lint` diff --git a/openspec/changes/prompt-command-contract-validation/.openspec.yaml b/openspec/changes/prompt-command-contract-validation/.openspec.yaml new file mode 100644 index 00000000..054b8c01 --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-08 diff --git a/openspec/changes/prompt-command-contract-validation/TDD_EVIDENCE.md b/openspec/changes/prompt-command-contract-validation/TDD_EVIDENCE.md new file mode 100644 index 00000000..329f2226 --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/TDD_EVIDENCE.md @@ -0,0 +1,30 @@ +# TDD Evidence + +## Failing Before + +- `hatch run pytest tests/unit/test_check_prompt_commands_script.py -q` — failed as expected before implementation. + - 7 failures. + - Missing `scripts/check-prompt-commands.py`. + - Missing docs-review workflow prompt validation trigger/step. + - Missing pre-commit prompt validation gate. + +## Passing After + +- `openspec validate prompt-command-contract-validation --strict` — passed. +- `hatch run pytest tests/unit/test_check_prompt_commands_script.py -q` — 7 passed. +- `hatch run validate-prompt-commands` — passed. +- `hatch run pytest tests/unit/test_check_prompt_commands_script.py tests/unit/test_pre_commit_quality_parity.py tests/unit/test_check_docs_commands_script.py tests/unit/test_validate_repo_manifests_bundle_deps.py tests/unit/test_registry_manifest_bundle_dependencies.py -q` — 30 passed. +- `hatch run yaml-lint` — passed. +- `hatch run format` — passed after applying formatter output. +- `hatch run type-check` — passed. +- `hatch run lint` — passed. +- `hatch run verify-modules-signature --payload-from-filesystem --enforce-version-bump --version-check-base HEAD` — passed. +- `hatch run contract-test` — 663 passed, 2 warnings. +- `hatch run smart-test` — 663 passed, 2 warnings. +- `hatch run test` — 663 passed, 2 warnings. +- `hatch run specfact code review run --bug-hunt --json --out .specfact/code-review.json --scope changed` — passed with zero findings. + +## Resource Signing and Registry Evidence + +- `hatch run sign-modules --payload-from-filesystem --changed-only --base-ref HEAD --bump-version patch --allow-unsigned` — refreshed changed bundle manifests and checksum integrity. +- `hatch run sync-registry-from-package --bundle specfact-backlog --bundle specfact-codebase --bundle specfact-govern --bundle specfact-project --bundle specfact-spec` — refreshed registry index and tarball sidecars for changed bundle versions. diff --git a/openspec/changes/prompt-command-contract-validation/design.md b/openspec/changes/prompt-command-contract-validation/design.md new file mode 100644 index 00000000..d0f1d552 --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/design.md @@ -0,0 +1,28 @@ +# Overview + +The existing docs validator already discovers mounted bundle command paths from Typer apps. This change extends that idea to shipped bundle prompts, because prompt resources are executable guidance consumed by AI IDEs and should fail fast when their command examples no longer match the installed CLI. + +## Design + +- Add `scripts/check-prompt-commands.py` as the prompt-specific validator. +- Reuse or mirror the command-discovery shape from `scripts/check-docs-commands.py`: import bundle app modules, convert Typer apps to Click commands, collect command paths, and inspect Click parameters for accepted options. +- Scan only `packages/*/resources/prompts/**/*.md`; `.github/prompts` is intentionally excluded. +- Extract command references from fenced shell blocks, inline backticks, slash prompt examples, and parameter/option prose. Resolve placeholders such as ``, `[]`, `[OPTIONS]`, line continuations, and comments without executing commands. +- Treat unknown command paths and unknown options as blocking findings with `path:line: [category] message` output. +- Require each executable prompt file to include a standard CLI reality-check/self-healing instruction. The current `_validate_cli_reality_check_guidance` implementation in `scripts/check-prompt-commands.py` enforces this independently for every prompt file and does not resolve companion prompt includes; companion include resolution is deferred until the validator has explicit include tracking. +- Add a Hatch script `validate-prompt-commands`. +- Wire `run_prompt_command_validation_gate` into `scripts/pre-commit-quality-checks.sh` before `check_safe_change` so prompt-only Markdown edits are still validated. +- Add CI execution to `.github/workflows/docs-review.yml` and include prompt resource paths in workflow triggers. +- Update `openspec/config.yaml` rules so future prompt-resource changes remember this validation surface. + +## Prompt Updates + +Prompt text should say: + +- Prompt instructions are operating guidance for SpecFact CLI, not the source of truth for the installed CLI. +- Before running a command, inspect the current command help when unsure. +- If a command or option from the prompt fails, inspect the nearest valid parent command with `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + +## Compatibility + +This change does not add runtime CLI commands for end users. It adds repository validation tooling and prompt resource edits. Because prompt resources are signed bundle payloads, implementation must account for module version/signature enforcement before finalization. diff --git a/openspec/changes/prompt-command-contract-validation/proposal.md b/openspec/changes/prompt-command-contract-validation/proposal.md new file mode 100644 index 00000000..67093242 --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/proposal.md @@ -0,0 +1,39 @@ +## Why + +Primary tester reports show that shipped AI slash prompts can drift from the CLI command surface. Because these prompts are bundle payloads, stale command paths or options can ship to users even when code, docs, and signatures pass. + +## What Changes + +- Add a prompt command validation gate for bundle-owned prompt resources under `packages/*/resources/prompts`. +- Validate prompt command examples and option references against the mounted Typer/Click command tree discovered from the current repo. +- Update shipped prompt guidance so prompts treat their text as operating guidance and verify CLI reality at execution time instead of acting as the source of truth. +- Wire the validator into Hatch, local pre-commit checks, and Markdown-triggered CI. +- Exclude `.github/prompts` from this change; OpenSpec helper prompts remain a separate governance surface. + +## Capabilities + +### New Capabilities + +- `prompt-command-validation`: Validation of bundle-owned AI prompt command references against current CLI command contracts. + +### Modified Capabilities + +- `bundle-packaged-resources`: Bundle-owned prompt resources are additionally required to be command-contract validated before release. +- `resource-aware-integrity`: Resource prompt edits remain signed payload changes and must continue to trigger version/signature enforcement. + +## Impact + +- Affected resources: `packages/*/resources/prompts/**/*.md`. +- Affected validation tooling: `scripts/`, `pyproject.toml`, `scripts/pre-commit-quality-checks.sh`, `.github/workflows/docs-review.yml`, and tests. +- Bundle prompt edits may require module version/signature updates because prompt resources are signed payloads; this change does not alter `registry/index.json` unless release packaging is performed separately. + +## Source Tracking + +- GitHub Issue: [#266](https://github.com/nold-ai/specfact-cli-modules/issues/266) +- Parent Feature: [#163](https://github.com/nold-ai/specfact-cli-modules/issues/163) +- Parent Epic: [#162](https://github.com/nold-ai/specfact-cli-modules/issues/162) +- Pull Request: [#265](https://github.com/nold-ai/specfact-cli-modules/pull/265) +- Project: SpecFact CLI (`Todo`) +- Labels: `enhancement`, `openspec`, `change-proposal` +- Blockers: none known +- Blocked by: none known diff --git a/openspec/changes/prompt-command-contract-validation/specs/bundle-packaged-resources/spec.md b/openspec/changes/prompt-command-contract-validation/specs/bundle-packaged-resources/spec.md new file mode 100644 index 00000000..5405c55c --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/specs/bundle-packaged-resources/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: Official bundles SHALL ship module-owned resource payloads + +Each official bundle package SHALL include the prompt templates and other non-code resources that are owned by that bundle's workflows or commands. Bundle-owned resources SHALL not depend on fallback storage under the core CLI repository, and shipped prompt resources SHALL pass prompt command validation before release. + +#### Scenario: Official bundles ship the audited prompt inventory + +- **WHEN** the audited prompt inventory from `RESOURCE_OWNERSHIP_AUDIT.md` is inspected +- **THEN** each prompt template's canonical packaged source exists under the owning official bundle package +- **AND** the ownership mapping covers the codebase, project, spec, govern, and backlog bundles for the currently supported prompt set + +#### Scenario: Backlog bundle ships the restored slash-prompt inventory + +- **WHEN** the backlog bundle package is inspected from source or from an installed artifact +- **THEN** `resources/prompts/` contains `specfact.backlog-add.md`, `specfact.backlog-daily.md`, `specfact.backlog-refine.md`, and `specfact.sync-backlog.md` +- **AND** those prompt files are treated as canonical bundle-owned sources rather than historical leftovers + +#### Scenario: Prompt companion resources ship with prompt payloads + +- **WHEN** an exported prompt template references a companion file by relative path, such as `./shared/cli-enforcement.md` +- **THEN** the owning bundle package contains that companion resource in a stable relative location +- **AND** prompt export/copy flows can preserve a resolvable relative layout in the target IDE workspace + +#### Scenario: Backlog bundle ships workspace-template seed resources + +- **WHEN** the backlog bundle package is inspected from source or from an installed artifact +- **THEN** the package contains the backlog field mapping templates that `specfact init` or related flows need to copy into workspace state +- **AND** the packaged set includes both ADO and non-ADO seed templates required by supported backlog flows + +#### Scenario: Core no longer remains the source of truth for bundle prompts + +- **WHEN** a workflow prompt belongs to an extracted bundle rather than to core lifecycle commands +- **THEN** that prompt's canonical packaged source exists in the owning bundle package +- **AND** release packaging does not rely on the core CLI repo as the canonical source for that prompt + +#### Scenario: Prompt resources are command-contract validated before release + +- **WHEN** a bundle prompt resource is changed +- **THEN** prompt command validation checks command paths and options against the current mounted CLI command tree +- **AND** release or PR validation fails until stale prompt command references are corrected diff --git a/openspec/changes/prompt-command-contract-validation/specs/prompt-command-validation/spec.md b/openspec/changes/prompt-command-contract-validation/specs/prompt-command-validation/spec.md new file mode 100644 index 00000000..9331efaa --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/specs/prompt-command-validation/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Bundle prompt command references SHALL match mounted CLI contracts + +The modules repository SHALL validate command paths and option names embedded in bundle-owned prompt resources against the mounted SpecFact CLI command tree discoverable from the current checkout. + +#### Scenario: Valid prompt command example passes + +- **GIVEN** a bundle prompt references an implemented command such as `specfact code repro --repo .` +- **WHEN** prompt command validation runs +- **THEN** the command path resolves through the mounted CLI command tree +- **AND** referenced options are accepted by that command or by an ancestor command context + +#### Scenario: Invalid prompt command path fails + +- **GIVEN** a bundle prompt references a stale command such as `specfact repro --repo .` +- **WHEN** prompt command validation runs +- **THEN** the validator reports the prompt path and line number +- **AND** the validation command exits non-zero + +#### Scenario: Invalid prompt option fails + +- **GIVEN** a bundle prompt references an option that is not accepted by the resolved command path +- **WHEN** prompt command validation runs +- **THEN** the validator reports the stale option, prompt path, and line number +- **AND** the validation command exits non-zero + +### Requirement: Prompt guidance SHALL self-check CLI reality + +Bundle-owned prompts SHALL tell AI IDE assistants that prompt text is operational guidance and that current CLI help or validation output is authoritative when prompt instructions disagree with the installed CLI. + +#### Scenario: Prompt contains CLI reality check guidance + +- **GIVEN** a shipped bundle prompt contains executable SpecFact command guidance +- **WHEN** prompt command validation inspects that prompt +- **THEN** the prompt includes a CLI reality-check instruction +- **AND** the prompt tells the assistant to prefer current CLI help over stale prompt prose + +#### Scenario: Broken prompt instruction gives self-healing behavior + +- **GIVEN** an assistant using a prompt finds that a referenced command or option is unavailable +- **WHEN** the prompt includes self-healing guidance +- **THEN** the assistant is instructed to inspect the nearest valid `--help` output +- **AND** continue only with a corrected command or ask the user when no safe correction is clear + +### Requirement: Prompt validation SHALL run in local and CI gates + +Prompt command validation SHALL be available as a Hatch command and SHALL run automatically in local pre-commit and Markdown/resource-triggered CI when bundle prompt resources or validation tooling change. + +#### Scenario: Hatch command exposes prompt validation + +- **WHEN** a contributor runs `hatch run validate-prompt-commands` +- **THEN** the bundle prompt validation script runs +- **AND** exits non-zero on any blocking prompt command finding + +#### Scenario: Pre-commit validates staged prompt edits before safe-change skipping + +- **GIVEN** a staged edit changes `packages/specfact-project/resources/prompts/specfact.02-plan.md` +- **WHEN** `scripts/pre-commit-quality-checks.sh block2` runs +- **THEN** prompt command validation runs before the script decides whether Block 2 can be skipped as a safe change + +#### Scenario: CI validates prompt edits + +- **GIVEN** a pull request changes a bundle prompt Markdown file +- **WHEN** the docs review workflow runs +- **THEN** prompt command validation runs with logs +- **AND** stale command references fail the workflow diff --git a/openspec/changes/prompt-command-contract-validation/specs/resource-aware-integrity/spec.md b/openspec/changes/prompt-command-contract-validation/specs/resource-aware-integrity/spec.md new file mode 100644 index 00000000..684583cd --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/specs/resource-aware-integrity/spec.md @@ -0,0 +1,21 @@ +## MODIFIED Requirements + +### Requirement: Bundle integrity SHALL cover resource payloads + +Bundle signing, verification, and publish validation SHALL treat bundled resource files as part of the signed module payload so that resource-only changes are detected as bundle changes. Prompt resource edits SHALL also be covered by prompt command validation before release. + +#### Scenario: Resource edit changes signed payload + +- **WHEN** a prompt template or other bundled resource file changes inside a bundle package +- **THEN** integrity verification detects a payload change until the manifest version and signature are refreshed + +#### Scenario: Resource-only change triggers version-bump enforcement + +- **WHEN** a bundled resource file changes but the bundle manifest version is not incremented +- **THEN** the modules-repo version-bump enforcement reports that the bundle payload changed without a version bump + +#### Scenario: Prompt resource edit triggers command-contract validation + +- **WHEN** a bundled prompt resource changes +- **THEN** the local and CI validation gates run prompt command validation +- **AND** stale command paths or options are reported before the changed resource can ship diff --git a/openspec/changes/prompt-command-contract-validation/tasks.md b/openspec/changes/prompt-command-contract-validation/tasks.md new file mode 100644 index 00000000..16d36afb --- /dev/null +++ b/openspec/changes/prompt-command-contract-validation/tasks.md @@ -0,0 +1,39 @@ +## 1. OpenSpec and Baseline + +- [x] 1.1 Create the `prompt-command-contract-validation` OpenSpec change. +- [x] 1.2 Add proposal, design, and spec deltas for prompt validation, packaged resources, and resource integrity. +- [x] 1.3 Validate the change with `openspec validate prompt-command-contract-validation --strict`. + +## 2. Tests First + +- [x] 2.1 Add unit tests for prompt command extraction from fenced shell blocks, inline backticks, slash examples, comments, placeholders, and line continuations. +- [x] 2.2 Add unit tests for invalid command path and invalid option findings. +- [x] 2.3 Add unit tests that prompt files with executable command guidance must contain CLI reality-check/self-healing guidance. +- [x] 2.4 Run the new tests before implementation and record failing evidence in `TDD_EVIDENCE.md`. + +## 3. Validator Implementation + +- [x] 3.1 Add `scripts/check-prompt-commands.py` to discover mounted command paths/options and validate `packages/*/resources/prompts/**/*.md`. +- [x] 3.2 Add `hatch run validate-prompt-commands`. +- [x] 3.3 Keep validator output deterministic and line-addressed for pre-commit and CI logs. + +## 4. Prompt Resource Repairs + +- [x] 4.1 Update bundle prompts to use current mounted command paths and option names. +- [x] 4.2 Add concise CLI reality-check/self-healing guidance to executable bundle prompts and shared companion guidance. +- [x] 4.3 Preserve bundle-owned resource layout and avoid `.github/prompts` changes in this scope. + +## 5. Local and CI Gates + +- [x] 5.1 Run prompt command validation from `scripts/pre-commit-quality-checks.sh` before safe-change skipping. +- [x] 5.2 Add prompt validation triggers and a logged validation step to `.github/workflows/docs-review.yml`. +- [x] 5.3 Update `openspec/config.yaml` rules to require prompt validation for prompt resource changes. + +## 6. Evidence and Finalization + +- [x] 6.1 Run `openspec validate prompt-command-contract-validation --strict`. +- [x] 6.2 Run focused pytest coverage for the prompt validator. +- [x] 6.3 Run `hatch run validate-prompt-commands`. +- [x] 6.4 Run relevant quality gates for touched scripts, YAML, prompts, and signed resources. +- [x] 6.5 Record failing-before and passing-after evidence in `TDD_EVIDENCE.md`. +- [x] 6.6 Resolve SpecFact code review findings or document any explicit exception. diff --git a/openspec/config.yaml b/openspec/config.yaml index 6a7e6415..5cfefa4a 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -72,6 +72,7 @@ rules: specs: - Use Given/When/Then for scenarios; tie scenarios to tests under `tests/` for bundle or registry behavior. - Call out new or changed Typer commands and Pydantic models with contract expectations where relevant. + - For changes touching `packages/*/resources/prompts`, include prompt command validation scenarios when command examples, options, or AI IDE operating guidance can drift from CLI reality. design: - Describe how the bundle integrates with `specfact_cli` imports and registry discovery—avoid circular or undeclared core dependencies. @@ -89,6 +90,7 @@ rules: For publicly tracked changes, include explicit readiness tasks for parent linkage, labels, project assignment, blockers, blocked-by relationships, and `in progress` concurrency verification before implementation begins. - Include module signing / version-bump tasks when `module-package.yaml` or bundle payloads change (see AGENTS.md). + - Include `hatch run validate-prompt-commands` when bundle prompt resources under `packages/*/resources/prompts` or prompt validation tooling changes. - Record TDD evidence in `openspec/changes//TDD_EVIDENCE.md` for behavior changes. - |- SpecFact code review JSON (dogfood, required before PR): Include tasks to diff --git a/packages/specfact-backlog/module-package.yaml b/packages/specfact-backlog/module-package.yaml index 32c2c9c3..efe230c5 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.24 +version: 0.41.25 commands: - backlog tier: official @@ -27,5 +27,5 @@ schema_extensions: project_metadata: - backlog_core.backlog_config integrity: - checksum: sha256:301afef315014fc2a2fd93c4104aae9c6f28b3145c6e3a97e2d5fc160e1adf73 - signature: qbVqb48u4wS727d5I7g6OIZMAFTv+aV10Y3uGIfJNZZhpoWptWsJ4BcVu5V034fULqso0Hb/7+lI4BrGMBeMCQ== + checksum: sha256:bd90c00d5b28c3c378f7824971bd5a8e870b5280e6837d447391816722cc6939 + signature: FIZD4ZyAfEM1SiuaxRNVfasEcpV6SaTZ+/7+mNJAvDNZzSdneC/DMiXXxTcHjVadkzLL6+7OHAEhuzXf5CZsCA== diff --git a/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md b/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md index 05e2227e..cd7b0a2e 100644 --- a/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-backlog/resources/prompts/shared/cli-enforcement.md @@ -1,5 +1,9 @@ # CLI Usage Enforcement Rules +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## Core Principle **ALWAYS use SpecFact CLI commands. Never create artifacts directly.** @@ -112,7 +116,7 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: - `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 enforce sdd []` - Validate SDD (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 - See [Command Reference](https://docs.specfact.io/reference/commands/) for full list diff --git a/packages/specfact-backlog/resources/prompts/specfact.backlog-add.md b/packages/specfact-backlog/resources/prompts/specfact.backlog-add.md index 84427891..550fdd43 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.backlog-add.md +++ b/packages/specfact-backlog/resources/prompts/specfact.backlog-add.md @@ -4,6 +4,10 @@ description: "Create backlog items with guided interactive flow and hierarchy ch # SpecFact Backlog Add Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text diff --git a/packages/specfact-backlog/resources/prompts/specfact.backlog-daily.md b/packages/specfact-backlog/resources/prompts/specfact.backlog-daily.md index e4675434..012c5133 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.backlog-daily.md +++ b/packages/specfact-backlog/resources/prompts/specfact.backlog-daily.md @@ -4,6 +4,10 @@ description: "Daily standup and sprint review with story-by-story walkthrough" # SpecFact Daily Standup Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text diff --git a/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md b/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md index 2d6f83f5..acfc3a30 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md +++ b/packages/specfact-backlog/resources/prompts/specfact.backlog-refine.md @@ -4,6 +4,10 @@ description: "Refine backlog items using template-driven AI assistance" # SpecFact Backlog Refinement Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -97,9 +101,9 @@ Refine backlog items from DevOps tools (GitHub Issues, Azure DevOps, etc.) into **Export/Import Workflow**: -1. Export items: `specfact backlog refine --adapter github --export-to-tmp --repo-owner OWNER --repo-name NAME` +1. Export items: `specfact backlog refine github --export-to-tmp --repo-owner OWNER --repo-name NAME` 2. Process with copilot: Open exported file and follow the embedded `## Copilot Instructions` and per-item template guidance (`Target Template`, `Required Sections`, `Optional Sections`). Save as `-refined.md` -3. Import refined: `specfact backlog refine --adapter github --import-from-tmp --repo-owner OWNER --repo-name NAME --write` +3. Import refined: `specfact backlog refine github --import-from-tmp --repo-owner OWNER --repo-name NAME --write` When refining from an exported file, treat the embedded instructions in that file as the source of truth for required structure and formatting. @@ -550,7 +554,7 @@ Items updated in remote backlog: **Error: "Azure DevOps API token required"** - **Cause**: Missing authentication token -- **Solution**: Provide token via `--ado-token`, `AZURE_DEVOPS_TOKEN` environment variable, or use `specfact auth azure-devops` for device code flow. +- **Solution**: Provide token via `--ado-token`, `AZURE_DEVOPS_TOKEN` environment variable, or use `specfact backlog auth azure-devops` for device code flow. ## Context diff --git a/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md b/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md index 83dc155e..4a3a8e71 100644 --- a/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md +++ b/packages/specfact-backlog/resources/prompts/specfact.sync-backlog.md @@ -1,5 +1,9 @@ # SpecFact Sync Backlog Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -267,7 +271,7 @@ specfact sync bridge --adapter ado --mode export-only --repo \ **Rules:** - Execute CLI first - never create artifacts directly -- Use `--no-interactive` flag in CI/CD environments +- Use only the non-interactive options shown by the current command help in CI/CD environments. - Never modify `.specfact/` or `openspec/` directly - Use CLI output as grounding for validation - Code generation requires LLM (only via AI IDE slash prompts, not CLI-only) diff --git a/packages/specfact-codebase/module-package.yaml b/packages/specfact-codebase/module-package.yaml index 406505ca..b50f0036 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.9 +version: 0.41.10 commands: - code tier: official @@ -24,5 +24,5 @@ description: Official SpecFact codebase bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:b7e6e2893e4398abca25c0bba4862663ceda8c7ae4df97ed1ad840f7eef3d2d5 - signature: xl1MEWdJFcA9PKmvEA0/cJV+X0Wv4lHIDxXUEsj+iSbfQ5TGs4c4TYd8/OWq6WjQDzq4vAC9/0m11PdpuNgZDQ== + checksum: sha256:f1a87d09b50ae91fd2e57986a04855796d658c704520b7d0889ccd4a00ad7b9a + signature: iVUCHYPzuJEIDOwPNmFZLEaAGmYj7E4vgLAT2N5SWwi+Jvzl5jsLgW86tbiChvKX1lwUrZtAS0sofZ2TwxVlDA== diff --git a/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md b/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md index b8aab9aa..c393f272 100644 --- a/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-codebase/resources/prompts/shared/cli-enforcement.md @@ -1,5 +1,9 @@ # CLI Usage Enforcement Rules +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## Core Principle **ALWAYS use SpecFact CLI commands. Never create artifacts directly.** @@ -112,7 +116,7 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: - `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 enforce sdd []` - Validate SDD (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 - See [Command Reference](../../docs/reference/commands.md) for full list diff --git a/packages/specfact-codebase/resources/prompts/specfact.01-import.md b/packages/specfact-codebase/resources/prompts/specfact.01-import.md index 388f628f..ecc5f44e 100644 --- a/packages/specfact-codebase/resources/prompts/specfact.01-import.md +++ b/packages/specfact-codebase/resources/prompts/specfact.01-import.md @@ -4,6 +4,10 @@ description: Import codebase → plan bundle. CLI extracts routes/schemas/relati # SpecFact Import Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -25,13 +29,13 @@ Import codebase → plan bundle. CLI extracts routes/schemas/relationships/contr ## Workflow -1. **Execute CLI**: `specfact [GLOBAL OPTIONS] import from-code [] --repo [options]` +1. **Execute CLI**: `specfact code import from-code [] --repo [options]` - CLI extracts: routes (FastAPI/Flask/Django), schemas (Pydantic), relationships, contracts (OpenAPI scaffolds), source tracking - Uses active plan if bundle not specified - - Note: `--no-interactive` is a global option and must appear before the subcommand (e.g., `specfact --no-interactive import from-code ...`). + - Inspect `specfact code import from-code --help` for the current non-interactive and enrichment options before running automation. - **Auto-enrichment enabled by default**: Automatically enhances vague acceptance criteria, incomplete requirements, and generic tasks using PlanEnricher (same logic as `plan review --auto-enrich`) - Use `--no-enrich-for-speckit` to disable auto-enrichment - - **Contract extraction**: OpenAPI contracts are extracted automatically **only** for features with `source_tracking.implementation_files` and detectable API endpoints (FastAPI/Flask patterns). For enrichment-added features or Django apps, use `specfact contract init` after enrichment (see Phase 4) + - **Contract extraction**: OpenAPI contracts are extracted automatically **only** for features with `source_tracking.implementation_files` and detectable API endpoints (FastAPI/Flask patterns). For enrichment-added features or Django apps, use `specfact spec contract init` after enrichment (see Phase 4) 2. **LLM Enrichment** (Copilot-only, before applying `--enrichment`): - Read CLI artifacts: `.specfact/projects//enrichment_context.md`, feature YAMLs, contract scaffolds, and brownfield reports @@ -49,7 +53,7 @@ Import codebase → plan bundle. CLI extracts routes/schemas/relationships/contr **Rules:** - Execute CLI first - never create artifacts directly -- Use the global `--no-interactive` flag in CI/CD environments (must appear before the subcommand) +- Use only the non-interactive options shown by the current command help in CI/CD environments. - Never modify `.specfact/` directly - Use CLI output as grounding for validation - Code generation requires LLM (only via AI IDE slash prompts, not CLI-only) @@ -62,7 +66,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact --no-interactive import from-code [] --repo +specfact code import from-code [] --repo ``` **Capture**: @@ -186,7 +190,7 @@ The enrichment parser expects a specific Markdown format. Follow this structure ```bash # Use enrichment to update plan via CLI -specfact --no-interactive import from-code [] --repo --enrichment +specfact code import from-code [] --repo --enrichment ``` **Result**: Final artifacts are CLI-generated with validated enrichments @@ -217,11 +221,11 @@ For features that need OpenAPI contracts (e.g., for sidecar validation with Cros ```bash # Generate contract for a single feature -specfact --no-interactive contract init --bundle --feature --repo +specfact spec contract init --bundle --feature --repo # Example: Generate contracts for all enrichment-added features -specfact --no-interactive contract init --bundle djangogoat-validation --feature FEATURE-USER-AUTHENTICATION --repo . -specfact --no-interactive contract init --bundle djangogoat-validation --feature FEATURE-NOTES-MANAGEMENT --repo . +specfact spec contract init --bundle djangogoat-validation --feature FEATURE-USER-AUTHENTICATION --repo . +specfact spec contract init --bundle djangogoat-validation --feature FEATURE-NOTES-MANAGEMENT --repo . # ... repeat for each feature that needs a contract ``` diff --git a/packages/specfact-codebase/resources/prompts/specfact.validate.md b/packages/specfact-codebase/resources/prompts/specfact.validate.md index 2548a8ee..30b44a47 100644 --- a/packages/specfact-codebase/resources/prompts/specfact.validate.md +++ b/packages/specfact-codebase/resources/prompts/specfact.validate.md @@ -4,6 +4,10 @@ description: Run full validation suite for reproducibility and contract complian # SpecFact Validate Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -50,7 +54,7 @@ Run full validation suite for reproducibility and contract compliance. Executes ### Step 2: Execute CLI ```bash -specfact repro --repo [--verbose] [--fail-fast] [--fix] [--budget ] [--out ] +specfact code repro --repo [--verbose] [--fail-fast] [--fix] [--budget ] [--out ] ``` ### Step 3: Present Results @@ -67,7 +71,7 @@ specfact repro --repo [--verbose] [--fail-fast] [--fix] [--budget [options] --no-interactive +specfact code repro --repo [options] ``` **Capture**: @@ -112,9 +116,8 @@ specfact repro --repo [options] --no-interactive ### Phase 3: CLI Artifact Creation (REQUIRED) ```bash -# Apply fixes via CLI commands, then re-validate -specfact plan update-feature [--bundle ] [options] --no-interactive -specfact repro --repo --no-interactive +# Apply code fixes, then re-validate with CLI +specfact code repro --repo [options] ``` **Result**: Final artifacts are CLI-generated with validated fixes diff --git a/packages/specfact-govern/module-package.yaml b/packages/specfact-govern/module-package.yaml index c2b8bd96..72d8573c 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.20 +version: 0.40.21 commands: - govern tier: official @@ -19,5 +19,5 @@ description: Official SpecFact governance bundle package. category: govern bundle_group_command: govern integrity: - checksum: sha256:ec360f43302550736cbef07fff66c51b0f8e6ca5062d515bb04d123b944e2560 - signature: LizVAjTgulXuKrDFlw+v4svJLc8mVpC6/fVb1Z7MaG+fRx6SwNUC/DylB7CaROFfrBYXtcRkHBu3MmjGzizHCw== + checksum: sha256:f56d74d4d87e77ad73b2978ac86adfeef969e74ac826b0ca7dd05d5fbf8c8fe5 + signature: omLeaJRZF6n+yq0CibNxgvp49i5AvzvGgIqtzRg5UzxpGaBV/PTjK4mPQi20YZfuD8DYzEr/AMxJDasH739QAQ== diff --git a/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md b/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md index b8aab9aa..c393f272 100644 --- a/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-govern/resources/prompts/shared/cli-enforcement.md @@ -1,5 +1,9 @@ # CLI Usage Enforcement Rules +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## Core Principle **ALWAYS use SpecFact CLI commands. Never create artifacts directly.** @@ -112,7 +116,7 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: - `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 enforce sdd []` - Validate SDD (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 - See [Command Reference](../../docs/reference/commands.md) for full list diff --git a/packages/specfact-govern/resources/prompts/specfact.05-enforce.md b/packages/specfact-govern/resources/prompts/specfact.05-enforce.md index 0d0c227b..cce91745 100644 --- a/packages/specfact-govern/resources/prompts/specfact.05-enforce.md +++ b/packages/specfact-govern/resources/prompts/specfact.05-enforce.md @@ -4,6 +4,10 @@ description: Validate SDD manifest against project bundle and contracts, check c # SpecFact SDD Enforcement Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -46,7 +50,7 @@ Validate SDD manifest against project bundle and contracts. Checks hash matching ### Step 2: Execute CLI ```bash -specfact enforce sdd [] [--sdd ] [--output-format ] [--out ] +specfact govern enforce sdd [] [--sdd ] [--output-format ] [--out ] # Uses active plan if bundle not specified ``` @@ -78,7 +82,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact enforce sdd [] [--sdd ] --no-interactive +specfact govern enforce sdd [] [--sdd ] --no-interactive ``` **Capture**: @@ -112,7 +116,7 @@ specfact enforce sdd [] [--sdd ] --no-interactive ```bash # Apply fixes via CLI commands, then re-validate specfact plan update-feature [--bundle ] [options] --no-interactive -specfact enforce sdd [] --no-interactive +specfact govern enforce sdd [] --no-interactive ``` **Result**: Final artifacts are CLI-generated with validated fixes diff --git a/packages/specfact-project/module-package.yaml b/packages/specfact-project/module-package.yaml index e090bdc1..1568de46 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.9 +version: 0.41.10 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:540ad1b4100cc701b9be8e3d2cd30823a9666083049fd9e4d91fd1aea5d383bd - signature: jvv0t3MwwWtlA8OrgW8xGZagcYuJSXBYznGWn6jYU2kcq2ABXTdlssY1sE1/BWhufXFwn3Y7nlI+r99rpMHPBA== + checksum: sha256:f436231e46a1b758321edf168f1a3d4f7e23dd1c0f2603de36fb6107b2f8f023 + signature: dbrACMxXY17pNGCNhh8KfzDPu2TUxY0SYkNsE3W/KZ3vMB0yo8+wkJgFtpgbYZsEJaJF8ZesK7luXb+I8ANMBw== diff --git a/packages/specfact-project/resources/prompts/shared/cli-enforcement.md b/packages/specfact-project/resources/prompts/shared/cli-enforcement.md index b8aab9aa..c393f272 100644 --- a/packages/specfact-project/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-project/resources/prompts/shared/cli-enforcement.md @@ -1,5 +1,9 @@ # CLI Usage Enforcement Rules +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## Core Principle **ALWAYS use SpecFact CLI commands. Never create artifacts directly.** @@ -112,7 +116,7 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: - `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 enforce sdd []` - Validate SDD (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 - See [Command Reference](../../docs/reference/commands.md) for full list diff --git a/packages/specfact-project/resources/prompts/specfact.02-plan.md b/packages/specfact-project/resources/prompts/specfact.02-plan.md index 66c7c010..3152deb6 100644 --- a/packages/specfact-project/resources/prompts/specfact.02-plan.md +++ b/packages/specfact-project/resources/prompts/specfact.02-plan.md @@ -4,6 +4,10 @@ description: Manage project bundles - create, add features/stories, and update p # SpecFact Plan Management Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -91,7 +95,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact plan [--bundle ] [options] --no-interactive +specfact plan [--bundle ] [options] ``` **Capture**: @@ -128,9 +132,9 @@ specfact plan [--bundle ] [options] --no-interactive ```bash # Use enrichment to update plan via CLI -specfact plan update-feature [--bundle ] --key [options] --no-interactive +specfact plan update-feature [--bundle ] --key [options] # Or use batch updates: -specfact plan update-feature [--bundle ] --batch-updates --no-interactive +specfact plan update-feature [--bundle ] --batch-updates ``` **Result**: Final artifacts are CLI-generated with validated enrichments diff --git a/packages/specfact-project/resources/prompts/specfact.03-review.md b/packages/specfact-project/resources/prompts/specfact.03-review.md index a66a6fed..3202e754 100644 --- a/packages/specfact-project/resources/prompts/specfact.03-review.md +++ b/packages/specfact-project/resources/prompts/specfact.03-review.md @@ -4,6 +4,10 @@ description: Review project bundle to identify ambiguities, resolve gaps, and pr # SpecFact Review Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -107,7 +111,7 @@ The recommendation helps less-experienced users make informed decisions. ### Behavior/Options - `--no-interactive` - Non-interactive mode (for CI/CD). Default: False (interactive mode) -- `--answers JSON` - JSON object with question_id -> answer mappings. Default: None +- `--answers PATH` - Path to a JSON file with question_id -> answer mappings. Default: None - `--auto-enrich` - Automatically enrich vague acceptance criteria using PlanEnricher (same enrichment logic as `import from-code`). Default: False (opt-in for review, but import has auto-enrichment enabled by default) **Important**: `--auto-enrich` will **NOT** resolve partial findings such as: @@ -145,7 +149,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 [] --list-questions --output-questions /tmp/questions.json --no-interactive +specfact plan review [] --list-questions --output-questions /tmp/questions.json # Uses active plan if bundle not specified ``` @@ -155,10 +159,10 @@ specfact plan review [] --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 [] --list-findings --findings-format json --no-interactive > /tmp/findings.json +specfact plan review [] --list-findings --findings-format json > /tmp/findings.json # Option 2: Save directly to file (recommended - clean JSON only) -specfact plan review [] --list-findings --output-findings /tmp/findings.json --no-interactive +specfact plan review [] --list-findings --output-findings /tmp/findings.json ``` **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. @@ -281,21 +285,21 @@ specfact plan review [] --list-findings --output-findings /tmp/find ```bash # Use /tmp/ to avoid polluting the codebase - specfact plan review [] --list-questions --output-questions /tmp/questions.json --no-interactive + specfact plan review [] --list-questions --output-questions /tmp/questions.json ``` 2. **LLM reasoning and user selection** (Step 3): - LLM presents questions with answer options **IN THE CHAT** - User selects answers (1-5, A-E, or custom text) - - **After user has selected all answers**, LLM adds selected answers to `/tmp/questions.json` + - **After user has selected all answers**, LLM exports selected answers to `/tmp/answers.json` 3. **Import answers via CLI** (after user selections are complete): ```bash # Import answers from exported file # Use /tmp/ to avoid polluting the codebase - specfact plan review [] --answers /tmp/answers.json --no-interactive + specfact plan review [] --answers /tmp/answers.json ``` **CRITICAL**: @@ -371,10 +375,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 [] --list-findings --output-findings /tmp/findings.json --no-interactive +specfact plan review [] --list-findings --output-findings /tmp/findings.json # Option 2: Get questions and save directly to /tmp/ (recommended - avoids JSON parsing) -specfact plan review [] --list-questions --output-questions /tmp/questions.json --no-interactive +specfact plan review [] --list-questions --output-questions /tmp/questions.json ``` **Capture**: @@ -451,7 +455,7 @@ specfact plan review [] --list-questions --output-questions /tmp/qu 4. **After user has selected all answers**: - - **THEN** add the selected answers to `/tmp/questions.json` in the `answers` object + - **THEN** export the selected answers to `/tmp/answers.json` - Map user selections (1-5) to the actual answer text from the options - If user selected a custom answer, use that text directly - **DO NOT** add answers to the file until user has selected all answers @@ -467,26 +471,26 @@ specfact plan review [] --list-questions --output-questions /tmp/qu - ❌ Write to `.specfact/` folder directly (always use CLI) - ❌ Create temporary files in project root (always use `/tmp/`) -**Output**: Updated `/tmp/questions.json` file with `answers` object populated +**Output**: `/tmp/answers.json` file with selected answers populated ### Phase 3: CLI Artifact Creation (REQUIRED) **For partial findings (REQUIRED workflow):** ```bash -# Import answers from /tmp/questions.json file +# Import answers from /tmp/answers.json file # Use /tmp/ to avoid polluting the codebase -specfact plan review [] --answers "$(jq -c '.answers' /tmp/questions.json)" --no-interactive +specfact plan review [] --answers /tmp/answers.json ``` **For non-partial findings only:** ```bash # Use auto-enrich for simple vague criteria (not partial findings) -specfact plan review [] --auto-enrich --no-interactive +specfact plan review [] --auto-enrich # Or use batch updates for feature updates -specfact plan update-feature [--bundle ] --batch-updates --no-interactive +specfact plan update-feature [--bundle ] --batch-updates ``` **Result**: Final artifacts are CLI-generated with validated enrichments @@ -537,7 +541,7 @@ Create one with: specfact plan init legacy-api /specfact.03-review --max-questions 10 # Ask more questions per session (up to 10) # Non-interactive with answers -/specfact.03-review --answers '{"Q001": "answer"}' # Provide answers directly +/specfact.03-review --answers /tmp/answers.json # Import answers from file /specfact.03-review --list-questions # Output questions as JSON to stdout /specfact.03-review --list-questions --output-questions /tmp/questions.json # Save questions to /tmp/ @@ -566,13 +570,13 @@ Create one with: specfact plan init legacy-api 1. **Export questions to file** (use `/tmp/` to avoid polluting codebase): ```bash - specfact plan review [] --list-questions --output-questions /tmp/questions.json --no-interactive + specfact plan review [] --list-questions --output-questions /tmp/questions.json ``` 2. **Get findings** (optional, for comprehensive analysis - use `/tmp/`): ```bash - specfact plan review [] --list-findings --output-findings /tmp/findings.json --no-interactive + specfact plan review [] --list-findings --output-findings /tmp/findings.json ``` 3. **LLM reasoning and user selection** (REQUIRED for partial findings): @@ -593,7 +597,7 @@ Create one with: specfact plan init legacy-api ```bash # Import answers from exported file - specfact plan review [] --answers /tmp/answers.json --no-interactive + specfact plan review [] --answers /tmp/answers.json ``` **CRITICAL**: Use the file path `/tmp/answers.json` (not a JSON string extracted from `/tmp/questions.json`) @@ -619,7 +623,7 @@ After applying enrichment or review updates, check if features need OpenAPI cont - Features added via enrichment typically don't have contracts (no `source_tracking`) - Django applications require manual contract generation (Django URL patterns not auto-detected) -- Use `specfact contract init --bundle --feature ` to generate contracts for features that need them +- Use `specfact spec contract init --bundle --feature ` to generate contracts for features that need them **Enrichment Report Format** (for `import from-code --enrichment`): diff --git a/packages/specfact-project/resources/prompts/specfact.04-sdd.md b/packages/specfact-project/resources/prompts/specfact.04-sdd.md index 6e406999..6dc53853 100644 --- a/packages/specfact-project/resources/prompts/specfact.04-sdd.md +++ b/packages/specfact-project/resources/prompts/specfact.04-sdd.md @@ -4,6 +4,10 @@ description: Create or update SDD manifest (hard spec) from project bundle with # SpecFact SDD Creation Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text diff --git a/packages/specfact-project/resources/prompts/specfact.06-sync.md b/packages/specfact-project/resources/prompts/specfact.06-sync.md index 4902781e..79aaef1b 100644 --- a/packages/specfact-project/resources/prompts/specfact.06-sync.md +++ b/packages/specfact-project/resources/prompts/specfact.06-sync.md @@ -4,6 +4,10 @@ description: Sync changes between external tool artifacts and SpecFact using bri # SpecFact Sync Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -91,7 +95,7 @@ specfact sync bridge --adapter ado --repo --ado-org --ado-project < **Rules:** - Execute CLI first - never create artifacts directly -- Use `--no-interactive` flag in CI/CD environments +- Use only the non-interactive options shown by the current command help in CI/CD environments. - Never modify `.specfact/` or `.specify/` directly - Use CLI output as grounding for validation - Code generation requires LLM (only via AI IDE slash prompts, not CLI-only) @@ -104,7 +108,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact sync bridge --adapter --repo [options] --no-interactive +specfact sync bridge --adapter --repo [options] ``` **Capture**: @@ -137,8 +141,8 @@ specfact sync bridge --adapter --repo [options] --no-interactiv ```bash # Apply resolutions via CLI commands, then re-sync -specfact plan update-feature [--bundle ] [options] --no-interactive -specfact sync bridge --adapter --repo --no-interactive +specfact plan update-feature [--bundle ] [options] +specfact sync bridge --adapter --repo ``` **Result**: Final artifacts are CLI-generated with validated resolutions diff --git a/packages/specfact-project/resources/prompts/specfact.compare.md b/packages/specfact-project/resources/prompts/specfact.compare.md index 637c1987..170e6a61 100644 --- a/packages/specfact-project/resources/prompts/specfact.compare.md +++ b/packages/specfact-project/resources/prompts/specfact.compare.md @@ -4,6 +4,10 @@ description: Compare manual and auto-derived plans to detect code vs plan drift # SpecFact Compare Command +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -65,7 +69,7 @@ specfact plan compare [--bundle ] [--manual ] [--auto ] **Rules:** - Execute CLI first - never create artifacts directly -- Use `--no-interactive` flag in CI/CD environments +- Use only the non-interactive options shown by the current command help in CI/CD environments. - Never modify `.specfact/` directly - Use CLI output as grounding for validation - Code generation requires LLM (only via AI IDE slash prompts, not CLI-only) @@ -78,7 +82,7 @@ When in copilot mode, follow this three-phase workflow: ```bash # Execute CLI to get structured output -specfact plan compare [--bundle ] [options] --no-interactive +specfact plan compare [--bundle ] [options] ``` **Capture**: @@ -111,8 +115,8 @@ specfact plan compare [--bundle ] [options] --no-interactive ```bash # Apply fixes via CLI commands, then re-compare -specfact plan update-feature [--bundle ] [options] --no-interactive -specfact plan compare [--bundle ] --no-interactive +specfact plan update-feature [--bundle ] [options] +specfact plan compare [--bundle ] ``` **Result**: Final artifacts are CLI-generated with validated fixes diff --git a/packages/specfact-spec/module-package.yaml b/packages/specfact-spec/module-package.yaml index c86bc115..65d1d255 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.17 +version: 0.40.18 commands: - spec tier: official @@ -21,5 +21,5 @@ description: Official SpecFact specification bundle package. category: spec bundle_group_command: spec integrity: - checksum: sha256:c5d058196cbf7cc871f350cfb6483ca59a28a299d16e78c330d7a21705575323 - signature: uWxT+qpVqTjF3BmO2rqdnSwII3YNQi3aA7UScZT6/PPN3Q0KvbtboVf1xLgIIp8/jjiWvks1dZFgXgyDNzozDg== + checksum: sha256:63731fe62322e59382430ddc27eeb372e0f4262b85e3351fce2503d658c042c9 + signature: bjIhapC/QeYOFtkGPS8aHIYc8S3ze4Xkv2XoXAJMf3OBu+uYC37Ytyh07wY6j1X9Gw9z+Ddj8u01f0gxxPO4Bw== diff --git a/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md b/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md index b8aab9aa..c393f272 100644 --- a/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md +++ b/packages/specfact-spec/resources/prompts/shared/cli-enforcement.md @@ -1,5 +1,9 @@ # CLI Usage Enforcement Rules +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## Core Principle **ALWAYS use SpecFact CLI commands. Never create artifacts directly.** @@ -112,7 +116,7 @@ When generating or enhancing code via LLM, **ALWAYS** follow this pattern: - `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 enforce sdd []` - Validate SDD (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 - See [Command Reference](../../docs/reference/commands.md) for full list diff --git a/packages/specfact-spec/resources/prompts/specfact.07-contracts.md b/packages/specfact-spec/resources/prompts/specfact.07-contracts.md index 0511859a..7de1d4cb 100644 --- a/packages/specfact-spec/resources/prompts/specfact.07-contracts.md +++ b/packages/specfact-spec/resources/prompts/specfact.07-contracts.md @@ -4,6 +4,10 @@ description: Analyze contract coverage, generate enhancement prompts, and apply # SpecFact Contract Enhancement Workflow +## CLI Reality Check + +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + ## User Input ```text @@ -42,7 +46,7 @@ Complete contract enhancement workflow: analyze coverage → generate prompts **First, identify files missing contracts:** ```bash -specfact analyze contracts --repo --bundle +specfact code analyze contracts --repo --bundle # Uses active plan if bundle not specified ``` @@ -70,7 +74,7 @@ specfact analyze contracts --repo --bundle **For each file missing contracts, generate a prompt:** ```bash -specfact generate contracts-prompt --apply --bundle +specfact spec generate contracts-prompt --apply --bundle ``` **Important:** @@ -132,7 +136,7 @@ Select files to enhance (comma-separated numbers, 'all', or 'skip'): **4.3: Validate enhanced code:** ```bash -specfact generate contracts-apply enhanced_.py --original +specfact spec generate contracts-apply enhanced_.py --original ``` **Validation includes:** @@ -198,7 +202,7 @@ Enhanced files: ... Next steps: -1. Verify contract coverage: specfact analyze contracts --bundle +1. Verify contract coverage: specfact code analyze contracts --bundle 2. Run full test suite: pytest (or your project's test command) 3. Review changes: git diff 4. Commit enhanced code @@ -226,7 +230,7 @@ This command **already implements** the standard validation loop pattern (see [C ```bash # CLI generates structured prompt -specfact generate contracts-prompt --apply --bundle +specfact spec generate contracts-prompt --apply --bundle ``` **Result**: Prompt saved to `.specfact/projects//prompts/enhance--.md` @@ -240,7 +244,7 @@ specfact generate contracts-prompt --apply --bundle ```bash # CLI validates temp file with all relevant tools -specfact generate contracts-apply enhanced_.py --original +specfact spec generate contracts-apply enhanced_.py --original ``` **Validation includes**: diff --git a/pyproject.toml b/pyproject.toml index 8078226d..39d19042 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ scan-all = "semgrep --config packages/specfact-code-review/.semgrep/clean_code.y # Modules-repo scoped parity commands 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" 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}" @@ -263,6 +264,7 @@ ignore = [ "tools/**/*" = ["T20", "S101", "INP001", "PLR2004"] "packages/**/commands.py" = ["B008"] "scripts/check-docs-commands.py" = ["N999"] +"scripts/check-prompt-commands.py" = ["N999"] [tool.ruff.lint.isort] force-single-line = false diff --git a/registry/index.json b/registry/index.json index b4b431c2..c626d5aa 100644 --- a/registry/index.json +++ b/registry/index.json @@ -2,9 +2,9 @@ "modules": [ { "id": "nold-ai/specfact-project", - "latest_version": "0.41.9", - "download_url": "modules/specfact-project-0.41.9.tar.gz", - "checksum_sha256": "e82f0e64d24e6890de6748cdb0566bf9afe82caf4c07bb346b8d8b4e8a19eb81", + "latest_version": "0.41.10", + "download_url": "modules/specfact-project-0.41.10.tar.gz", + "checksum_sha256": "eee055c4fecf112360f2265f05693eca8848e4db77a3b785104e31876d9f2a3f", "tier": "official", "publisher": { "name": "nold-ai", @@ -16,9 +16,9 @@ }, { "id": "nold-ai/specfact-backlog", - "latest_version": "0.41.24", - "download_url": "modules/specfact-backlog-0.41.24.tar.gz", - "checksum_sha256": "28fbd651ddd99e70ca919264a6771ce60dddc7865283583be90b374135a0488b", + "latest_version": "0.41.25", + "download_url": "modules/specfact-backlog-0.41.25.tar.gz", + "checksum_sha256": "47b0150fdd404e813e7cb720b6d411af6aff682d9526fea59599ae84c430ccf3", "tier": "official", "publisher": { "name": "nold-ai", @@ -30,9 +30,9 @@ }, { "id": "nold-ai/specfact-codebase", - "latest_version": "0.41.9", - "download_url": "modules/specfact-codebase-0.41.9.tar.gz", - "checksum_sha256": "5aeec7735644108ae1861a7f1913d38761ae37612d76bfa145131e37869704c9", + "latest_version": "0.41.10", + "download_url": "modules/specfact-codebase-0.41.10.tar.gz", + "checksum_sha256": "413d65ec97f6339759f58e6279ca758e120d1143f75a747333879d0bac1967bb", "tier": "official", "publisher": { "name": "nold-ai", @@ -46,9 +46,9 @@ }, { "id": "nold-ai/specfact-spec", - "latest_version": "0.40.17", - "download_url": "modules/specfact-spec-0.40.17.tar.gz", - "checksum_sha256": "a793088c0f9b4958a9dc939b51d004c8257b53fb584218b82beb0c7e0f39d848", + "latest_version": "0.40.18", + "download_url": "modules/specfact-spec-0.40.18.tar.gz", + "checksum_sha256": "ed5c6f85b10aa1c20e5f6cb9542a61ea9d97c6e2fe43cf3ed256243060461cd2", "tier": "official", "publisher": { "name": "nold-ai", @@ -62,9 +62,9 @@ }, { "id": "nold-ai/specfact-govern", - "latest_version": "0.40.20", - "download_url": "modules/specfact-govern-0.40.20.tar.gz", - "checksum_sha256": "8f58e1c0194f4915301d7946e3aed65f521d1f03a011287267f40244f45eaf89", + "latest_version": "0.40.21", + "download_url": "modules/specfact-govern-0.40.21.tar.gz", + "checksum_sha256": "ae26f31554613eae5977ceb40073928475c4c89fbbd01978a349e06eb7e1e57a", "tier": "official", "publisher": { "name": "nold-ai", diff --git a/registry/modules/specfact-backlog-0.41.25.tar.gz b/registry/modules/specfact-backlog-0.41.25.tar.gz new file mode 100644 index 00000000..90401c7f Binary files /dev/null and b/registry/modules/specfact-backlog-0.41.25.tar.gz differ diff --git a/registry/modules/specfact-backlog-0.41.25.tar.gz.sha256 b/registry/modules/specfact-backlog-0.41.25.tar.gz.sha256 new file mode 100644 index 00000000..c3cf085e --- /dev/null +++ b/registry/modules/specfact-backlog-0.41.25.tar.gz.sha256 @@ -0,0 +1 @@ +47b0150fdd404e813e7cb720b6d411af6aff682d9526fea59599ae84c430ccf3 diff --git a/registry/modules/specfact-codebase-0.41.10.tar.gz b/registry/modules/specfact-codebase-0.41.10.tar.gz new file mode 100644 index 00000000..6cbbf704 Binary files /dev/null and b/registry/modules/specfact-codebase-0.41.10.tar.gz differ diff --git a/registry/modules/specfact-codebase-0.41.10.tar.gz.sha256 b/registry/modules/specfact-codebase-0.41.10.tar.gz.sha256 new file mode 100644 index 00000000..74c62a73 --- /dev/null +++ b/registry/modules/specfact-codebase-0.41.10.tar.gz.sha256 @@ -0,0 +1 @@ +413d65ec97f6339759f58e6279ca758e120d1143f75a747333879d0bac1967bb diff --git a/registry/modules/specfact-govern-0.40.21.tar.gz b/registry/modules/specfact-govern-0.40.21.tar.gz new file mode 100644 index 00000000..62e08005 Binary files /dev/null and b/registry/modules/specfact-govern-0.40.21.tar.gz differ diff --git a/registry/modules/specfact-govern-0.40.21.tar.gz.sha256 b/registry/modules/specfact-govern-0.40.21.tar.gz.sha256 new file mode 100644 index 00000000..5f82b6a3 --- /dev/null +++ b/registry/modules/specfact-govern-0.40.21.tar.gz.sha256 @@ -0,0 +1 @@ +ae26f31554613eae5977ceb40073928475c4c89fbbd01978a349e06eb7e1e57a diff --git a/registry/modules/specfact-project-0.41.10.tar.gz b/registry/modules/specfact-project-0.41.10.tar.gz new file mode 100644 index 00000000..df4ccae7 Binary files /dev/null and b/registry/modules/specfact-project-0.41.10.tar.gz differ diff --git a/registry/modules/specfact-project-0.41.10.tar.gz.sha256 b/registry/modules/specfact-project-0.41.10.tar.gz.sha256 new file mode 100644 index 00000000..4a9bd9ae --- /dev/null +++ b/registry/modules/specfact-project-0.41.10.tar.gz.sha256 @@ -0,0 +1 @@ +eee055c4fecf112360f2265f05693eca8848e4db77a3b785104e31876d9f2a3f diff --git a/registry/modules/specfact-spec-0.40.18.tar.gz b/registry/modules/specfact-spec-0.40.18.tar.gz new file mode 100644 index 00000000..39731e03 Binary files /dev/null and b/registry/modules/specfact-spec-0.40.18.tar.gz differ diff --git a/registry/modules/specfact-spec-0.40.18.tar.gz.sha256 b/registry/modules/specfact-spec-0.40.18.tar.gz.sha256 new file mode 100644 index 00000000..f37f3ae0 --- /dev/null +++ b/registry/modules/specfact-spec-0.40.18.tar.gz.sha256 @@ -0,0 +1 @@ +ed5c6f85b10aa1c20e5f6cb9542a61ea9d97c6e2fe43cf3ed256243060461cd2 diff --git a/registry/signatures/specfact-backlog-0.41.25.tar.sig b/registry/signatures/specfact-backlog-0.41.25.tar.sig new file mode 100644 index 00000000..2f6423db --- /dev/null +++ b/registry/signatures/specfact-backlog-0.41.25.tar.sig @@ -0,0 +1 @@ +FIZD4ZyAfEM1SiuaxRNVfasEcpV6SaTZ+/7+mNJAvDNZzSdneC/DMiXXxTcHjVadkzLL6+7OHAEhuzXf5CZsCA== diff --git a/registry/signatures/specfact-codebase-0.41.10.tar.sig b/registry/signatures/specfact-codebase-0.41.10.tar.sig new file mode 100644 index 00000000..4abd6d46 --- /dev/null +++ b/registry/signatures/specfact-codebase-0.41.10.tar.sig @@ -0,0 +1 @@ +iVUCHYPzuJEIDOwPNmFZLEaAGmYj7E4vgLAT2N5SWwi+Jvzl5jsLgW86tbiChvKX1lwUrZtAS0sofZ2TwxVlDA== diff --git a/registry/signatures/specfact-govern-0.40.21.tar.sig b/registry/signatures/specfact-govern-0.40.21.tar.sig new file mode 100644 index 00000000..be0773c6 --- /dev/null +++ b/registry/signatures/specfact-govern-0.40.21.tar.sig @@ -0,0 +1 @@ +omLeaJRZF6n+yq0CibNxgvp49i5AvzvGgIqtzRg5UzxpGaBV/PTjK4mPQi20YZfuD8DYzEr/AMxJDasH739QAQ== diff --git a/registry/signatures/specfact-project-0.41.10.tar.sig b/registry/signatures/specfact-project-0.41.10.tar.sig new file mode 100644 index 00000000..e4be05c0 --- /dev/null +++ b/registry/signatures/specfact-project-0.41.10.tar.sig @@ -0,0 +1 @@ +dbrACMxXY17pNGCNhh8KfzDPu2TUxY0SYkNsE3W/KZ3vMB0yo8+wkJgFtpgbYZsEJaJF8ZesK7luXb+I8ANMBw== diff --git a/registry/signatures/specfact-spec-0.40.18.tar.sig b/registry/signatures/specfact-spec-0.40.18.tar.sig new file mode 100644 index 00000000..f4c9ee8e --- /dev/null +++ b/registry/signatures/specfact-spec-0.40.18.tar.sig @@ -0,0 +1 @@ +bjIhapC/QeYOFtkGPS8aHIYc8S3ze4Xkv2XoXAJMf3OBu+uYC37Ytyh07wY6j1X9Gw9z+Ddj8u01f0gxxPO4Bw== diff --git a/scripts/check-prompt-commands.py b/scripts/check-prompt-commands.py new file mode 100644 index 00000000..ffed46f4 --- /dev/null +++ b/scripts/check-prompt-commands.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Validate shipped bundle prompt command references against mounted CLI contracts.""" + +from __future__ import annotations + +import argparse +import importlib +import re +import sys +from collections.abc import Iterable +from pathlib import Path +from typing import NamedTuple + +import click +from typer.main import get_command as typer_get_command + + +REPO_ROOT = Path(__file__).resolve().parents[1] +PROMPT_ROOT = REPO_ROOT / "packages" +INLINE_COMMAND_RE = re.compile(r"`(/?specfact(?:[\s.][^`\n]*)?)`") +OPTION_RE = re.compile(r"(?>>|>|[A-Za-z]:\\>|[^\s@]+@[^:]+:[^$#>]*[$#])\s*") +IGNORED_OPTIONS = frozenset({"--help"}) +SKIP_OPTION_VALIDATION = frozenset({"[OPTIONS]", "[options]", "[ARGS]"}) +REQUIRED_GUIDANCE_SNIPPETS = ( + "operating guidance", + "not the source of truth", + "CLI help is authoritative", + "--help", + "ask the user", +) +# Keep this explicit until bundle command metadata exposes prompt-validator mounts. +MODULE_APP_MOUNTS = ( + ("specfact_backlog.backlog.commands", "app", ("specfact", "backlog")), + ("specfact_backlog.policy_engine.commands", "app", ("specfact", "backlog", "policy")), + ("specfact_codebase.code.commands", "app", ("specfact", "code")), + ("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")), + ("specfact_spec.generate.commands", "app", ("specfact", "spec", "generate")), +) +CORE_COMMAND_PATHS = frozenset( + { + ("specfact",), + ("specfact", "init"), + ("specfact", "module"), + ("specfact", "upgrade"), + } +) + + +class PromptCommandExample(NamedTuple): + source: Path + line_number: int + text: str + + +class ValidationFinding(NamedTuple): + category: str + source: Path + line_number: int + message: str + + +class CommandIndex(NamedTuple): + command_paths: set[tuple[str, ...]] + options_by_path: dict[tuple[str, ...], set[str]] + + +def _script_name(path: Path) -> str: + try: + return str(path.relative_to(REPO_ROOT)) + except ValueError: + return str(path) + + +def _ensure_package_paths() -> None: + 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 _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()) + return paths + + +def _load_texts(paths: Iterable[Path]) -> dict[Path, str]: + return {path: path.read_text(encoding="utf-8") for path in paths} + + +def _command_options(command: click.Command) -> set[str]: + options: set[str] = set() + for param in command.params: + if isinstance(param, click.Option): + options.update(opt for opt in param.opts if opt.startswith("--")) + options.update(opt for opt in param.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): + return + for name, child in command.commands.items(): + _collect_click_index(child, (*prefix, name), index) + + +def _build_command_index() -> CommandIndex: + _ensure_package_paths() + index = CommandIndex( + command_paths=set(CORE_COMMAND_PATHS), options_by_path={path: set() for path in CORE_COMMAND_PATHS} + ) + for module_name, attr_name, prefix in MODULE_APP_MOUNTS: + try: + module = importlib.import_module(module_name) + app = getattr(module, attr_name) + 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 + _collect_click_index(click_command, prefix, index) + return index + + +def _strip_comment(line: str) -> str: + if line.lstrip().startswith("#"): + return "" + return re.sub(r"\s+#.*$", "", line).strip() + + +def _starts_with_command(line: str) -> bool: + stripped = _strip_shell_prompt(line.strip()) + return stripped.startswith(COMMAND_STARTS) + + +def _strip_shell_prompt(line: str) -> str: + return SHELL_PROMPT_RE.sub("", line, count=1).strip() + + +def _normalize_prompt_command(raw: str) -> str: + command = raw.strip().rstrip(":,") + if command.startswith("/"): + command = command[1:] + return " ".join(command.split()) + + +def _iter_fenced_command_examples(text: str, source: Path) -> list[PromptCommandExample]: + examples: list[PromptCommandExample] = [] + in_shell_block = False + pending = "" + pending_line = 0 + for line_number, raw_line in enumerate(text.splitlines(), start=1): + stripped = raw_line.strip() + if stripped.startswith("```"): + fence_parts = stripped.removeprefix("```").split(maxsplit=1) + fence_language = fence_parts[0].lower() if fence_parts else "" + if in_shell_block: + in_shell_block = False + pending = "" + pending_line = 0 + else: + in_shell_block = fence_language in {"bash", "sh", "shell", "zsh", "console"} + continue + if not in_shell_block: + continue + line = _strip_comment(stripped) + if not line: + continue + line = _strip_shell_prompt(line) + continued = line.rstrip("\\").strip() + if pending: + pending = f"{pending} {continued}" + elif _starts_with_command(line): + pending = continued + pending_line = line_number + else: + continue + if line.endswith("\\"): + continue + examples.append(PromptCommandExample(source, pending_line, _normalize_prompt_command(pending))) + pending = "" + pending_line = 0 + return examples + + +def _iter_inline_command_examples(text: str, source: Path) -> list[PromptCommandExample]: + examples: list[PromptCommandExample] = [] + for line_number, raw_line in enumerate(text.splitlines(), start=1): + for match in INLINE_COMMAND_RE.finditer(raw_line): + command = _normalize_prompt_command(match.group(1)) + if command.startswith("specfact"): + examples.append(PromptCommandExample(source, line_number, command)) + 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]: + key = (example.line_number, example.text) + if key in seen: + continue + seen.add(key) + examples.append(example) + return examples + + +def _command_tokens(command_text: str) -> list[str]: + tokens = command_text.split() + if not tokens: + return [] + # Tokens containing dots are IDE slash-command shortcuts, not CLI command paths. + if "." in tokens[0]: + return [] + return tokens + + +def _resolve_command_path(command_text: str, command_paths: set[tuple[str, ...]]) -> tuple[str, ...] | None: + tokens = _command_tokens(command_text) + if not tokens or tokens[0] != "specfact": + return None + command_words: list[str] = [] + for token in tokens: + if token.startswith(("-", "[", "<")): + break + command_words.append(token) + for length in range(len(command_words), 0, -1): + candidate = tuple(command_words[:length]) + if candidate in command_paths: + if len(candidate) < len(command_words) and _has_subcommands(candidate, command_paths): + return None + return candidate + return None + + +def _has_subcommands(path: tuple[str, ...], command_paths: set[tuple[str, ...]]) -> bool: + return any(len(candidate) > len(path) and candidate[: len(path)] == path for candidate in command_paths) + + +def _unknown_command_finding(example: PromptCommandExample) -> ValidationFinding: + return ValidationFinding( + category="command", + source=example.source, + line_number=example.line_number, + message=f"Unknown prompt command example: {example.text}", + ) + + +def _options_for_path(path: tuple[str, ...], index: CommandIndex) -> set[str]: + options: set[str] = set(IGNORED_OPTIONS) + for length in range(1, len(path) + 1): + options.update(index.options_by_path.get(tuple(path[:length]), set())) + return options + + +def _unknown_option_findings( + example: PromptCommandExample, path: tuple[str, ...], index: CommandIndex +) -> list[ValidationFinding]: + if any(marker in example.text for marker in SKIP_OPTION_VALIDATION): + return [] + allowed_options = _options_for_path(path, index) + findings: list[ValidationFinding] = [] + for option in sorted(set(OPTION_RE.findall(example.text))): + if option in allowed_options: + continue + findings.append( + ValidationFinding( + category="option", + source=example.source, + line_number=example.line_number, + message=f"Unknown option for {' '.join(path)}: {option} in {example.text}", + ) + ) + return findings + + +def _validate_prompt_command_examples( + text_by_path: dict[Path, str], command_index: CommandIndex +) -> list[ValidationFinding]: + findings: list[ValidationFinding] = [] + for path, text in text_by_path.items(): + for example in _extract_prompt_command_examples_from_text(text, path): + if not _command_tokens(example.text): + continue + resolved_path = _resolve_command_path(example.text, command_index.command_paths) + if resolved_path is None: + findings.append(_unknown_command_finding(example)) + continue + findings.extend(_unknown_option_findings(example, resolved_path, command_index)) + return findings + + +def _has_executable_prompt_command(text: str, source: Path) -> bool: + return any(_command_tokens(example.text) for example in _extract_prompt_command_examples_from_text(text, source)) + + +def _has_cli_reality_check(text: str) -> bool: + normalized = " ".join(text.split()).lower() + return all(snippet.lower() in normalized for snippet in REQUIRED_GUIDANCE_SNIPPETS) + + +def _validate_cli_reality_check_guidance(text_by_path: dict[Path, str]) -> list[ValidationFinding]: + findings: list[ValidationFinding] = [] + for path, text in text_by_path.items(): + if not _has_executable_prompt_command(text, path): + continue + if _has_cli_reality_check(text): + continue + findings.append( + ValidationFinding( + category="guidance", + source=path, + line_number=1, + message="Missing CLI reality-check/self-healing guidance for executable prompt commands", + ) + ) + return findings + + +def _format_findings(findings: list[ValidationFinding]) -> str: + return "\n".join( + f"{_script_name(finding.source)}:{finding.line_number}: [{finding.category}] {finding.message}" + for finding in findings + ) + + +def _parse_args(argv: list[str] | None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate bundle prompt command references.") + parser.add_argument( + "paths", + nargs="*", + type=Path, + help="Prompt files to validate. Defaults to packages/*/resources/prompts/**/*.md.", + ) + return parser.parse_args(argv) + + +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()] + + +def _main(argv: list[str] | None = None) -> int: + args = _parse_args(argv) + paths = _selected_paths(args) + text_by_path = _load_texts(paths) + command_index = _build_command_index() + findings = [ + *_validate_prompt_command_examples(text_by_path, command_index), + *_validate_cli_reality_check_guidance(text_by_path), + ] + if findings: + sys.stderr.write(_format_findings(findings) + "\n") + return 1 + sys.stdout.write("Prompt command validation passed with no findings.\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(_main()) diff --git a/scripts/pre-commit-quality-checks.sh b/scripts/pre-commit-quality-checks.sh index 8418d66e..8edc6fba 100755 --- a/scripts/pre-commit-quality-checks.sh +++ b/scripts/pre-commit-quality-checks.sh @@ -85,6 +85,18 @@ staged_docs_validation_paths() { done < <(staged_files) } +staged_prompt_validation_paths() { + local line + 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) + printf '%s\n' "${line}" + ;; + esac + done < <(staged_files) +} + needs_docs_site_validation() { local line while IFS= read -r line; do @@ -94,6 +106,15 @@ needs_docs_site_validation() { return 1 } +needs_prompt_command_validation() { + local line + while IFS= read -r line; do + [ -z "${line}" ] && continue + return 0 + done < <(staged_prompt_validation_paths) + return 1 +} + run_docs_site_validation_gate() { if ! needs_docs_site_validation; then return 0 @@ -108,6 +129,20 @@ run_docs_site_validation_gate() { fi } +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 + success "✅ Prompt command validation passed" + else + error "❌ Prompt command validation failed" + warn "💡 Run: hatch run validate-prompt-commands" + exit 1 + fi +} + check_safe_change() { local files files=$(staged_files) @@ -258,6 +293,7 @@ run_block1_lint() { run_block2() { warn "🔍 modules pre-commit — Block 2 — hook: review + contract tests" run_docs_site_validation_gate + run_prompt_command_validation_gate if check_safe_change; then success "✅ Safe change detected — skipping Block 2 (code review + contract tests)" info "💡 Only docs, workflow, version, or pre-commit metadata changed" @@ -277,6 +313,7 @@ run_all() { run_lint_if_staged_python success "✅ Block 1 complete (all stages passed or skipped as expected)" run_docs_site_validation_gate + run_prompt_command_validation_gate if check_safe_change; then success "✅ Safe change detected — skipping Block 2 (code review + contract tests)" info "💡 Only docs, workflow, version, or pre-commit metadata changed" diff --git a/tests/unit/test_check_prompt_commands_script.py b/tests/unit/test_check_prompt_commands_script.py new file mode 100644 index 00000000..a132828e --- /dev/null +++ b/tests/unit/test_check_prompt_commands_script.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.unit._script_test_utils import load_module_from_path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_PATH = REPO_ROOT / "scripts" / "check-prompt-commands.py" +DOCS_REVIEW_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "docs-review.yml" +PRE_COMMIT_SCRIPT = REPO_ROOT / "scripts" / "pre-commit-quality-checks.sh" + + +def _load_script(): + return load_module_from_path("check_prompt_commands", SCRIPT_PATH) + + +def _script_attr(script, name: str): + return getattr(script, name) + + +def _write_prompt(tmp_path: Path, text: str) -> Path: + prompt = tmp_path / "packages" / "specfact-codebase" / "resources" / "prompts" / "specfact.validate.md" + prompt.parent.mkdir(parents=True) + prompt.write_text(text.strip() + "\n", encoding="utf-8") + return prompt + + +def test_extract_prompt_command_examples_handles_common_prompt_syntax(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +# Prompt + +Run `/specfact.validate --repo .` when using the IDE shortcut. + +```bash +# comments are ignored +$ specfact code repro --repo /tmp/project +specfact code repro --repo . \\ + --budget 120 +``` + +- `specfact govern enforce sdd --no-interactive` +""", + ) + + examples = _script_attr(script, "_extract_prompt_command_examples_from_text")( + prompt.read_text(encoding="utf-8"), prompt + ) + + assert [example.text for example in examples] == [ + "specfact code repro --repo /tmp/project", + "specfact code repro --repo . --budget 120", + "specfact govern enforce sdd --no-interactive", + "specfact.validate --repo .", + ] + + +def test_validate_prompt_commands_reports_stale_command_path(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +Prompt instructions are operating guidance. Current CLI help is authoritative; if this prompt drifts, inspect `--help`. + +```bash +specfact repro --repo . +``` +""", + ) + command_index = _script_attr(script, "CommandIndex")( + command_paths={("specfact",), ("specfact", "code"), ("specfact", "code", "repro")}, + options_by_path={("specfact", "code", "repro"): {"--repo"}}, + ) + + findings = _script_attr(script, "_validate_prompt_command_examples")( + {prompt: prompt.read_text(encoding="utf-8")}, + command_index, + ) + + assert len(findings) == 1 + assert findings[0].category == "command" + assert "specfact repro --repo ." in findings[0].message + + +def test_validate_prompt_commands_reports_stale_nested_subcommand_path(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +Prompt instructions are operating guidance. Current CLI help is authoritative; if this prompt drifts, inspect `--help`. + +```bash +specfact code stale-subcmd --repo . +``` +""", + ) + command_index = _script_attr(script, "CommandIndex")( + command_paths={("specfact",), ("specfact", "code"), ("specfact", "code", "repro")}, + options_by_path={("specfact", "code", "repro"): {"--repo"}}, + ) + + findings = _script_attr(script, "_validate_prompt_command_examples")( + {prompt: prompt.read_text(encoding="utf-8")}, + command_index, + ) + + assert len(findings) == 1 + assert findings[0].category == "command" + assert "specfact code stale-subcmd --repo ." in findings[0].message + + +def test_main_writes_findings_to_stderr(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. +Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, +correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + +```bash +specfact repro --repo . +``` +""", + ) + + assert _script_attr(script, "_main")([str(prompt)]) == 1 + + captured = capsys.readouterr() + assert captured.out == "" + assert "Unknown prompt command example: specfact repro --repo ." in captured.err + + +def test_validate_prompt_commands_reports_stale_option(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +Prompt instructions are operating guidance. Current CLI help is authoritative; if this prompt drifts, inspect `--help`. + +`specfact code repro --repo . --missing-option` +""", + ) + command_index = _script_attr(script, "CommandIndex")( + command_paths={("specfact",), ("specfact", "code"), ("specfact", "code", "repro")}, + options_by_path={("specfact", "code", "repro"): {"--repo", "--help"}}, + ) + + findings = _script_attr(script, "_validate_prompt_command_examples")( + {prompt: prompt.read_text(encoding="utf-8")}, + command_index, + ) + + assert len(findings) == 1 + assert findings[0].category == "option" + assert "--missing-option" in findings[0].message + + +def test_validate_prompt_guidance_requires_cli_reality_check(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +```bash +specfact code repro --repo . +``` +""", + ) + + findings = _script_attr(script, "_validate_cli_reality_check_guidance")( + {prompt: prompt.read_text(encoding="utf-8")} + ) + + assert len(findings) == 1 + assert findings[0].category == "guidance" + assert "CLI reality-check" in findings[0].message + + +def test_validate_prompt_guidance_accepts_self_healing_language(tmp_path: Path) -> None: + script = _load_script() + prompt = _write_prompt( + tmp_path, + """ +Prompt instructions are operating guidance for SpecFact CLI, not the source of truth. +Current CLI help is authoritative. If a command or option fails, inspect the nearest valid `--help`, +correct the invocation when the mapping is obvious, and ask the user when no safe correction is clear. + +```bash +specfact code repro --repo . +``` +""", + ) + + findings = _script_attr(script, "_validate_cli_reality_check_guidance")( + {prompt: prompt.read_text(encoding="utf-8")} + ) + + assert not findings + + +def test_build_command_index_reports_failed_mount_context(monkeypatch: pytest.MonkeyPatch) -> None: + script = _load_script() + + monkeypatch.setattr(script, "MODULE_APP_MOUNTS", (("missing.module", "app", ("specfact", "missing")),)) + + with pytest.raises(RuntimeError) as exc_info: + _script_attr(script, "_build_command_index")() + + message = str(exc_info.value) + assert "missing.module:app" in message + assert "specfact missing" in message + + +def test_module_app_mounts_include_govern_enforce_app() -> None: + script = _load_script() + + assert ( + "specfact_govern.enforce.commands", + "app", + ("specfact", "govern", "enforce"), + ) in _script_attr(script, "MODULE_APP_MOUNTS") + + +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 "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 + + +def test_pre_commit_runs_prompt_validation_before_safe_change_skip() -> None: + script = PRE_COMMIT_SCRIPT.read_text(encoding="utf-8") + + 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 + + +def test_pre_commit_prompt_validation_covers_cli_command_implementations() -> None: + script = PRE_COMMIT_SCRIPT.read_text(encoding="utf-8") + + assert "packages/*/src/**/commands.py" in script diff --git a/tests/unit/workflows/test_publish_modules_workflow.py b/tests/unit/workflows/test_publish_modules_workflow.py new file mode 100644 index 00000000..cc105233 --- /dev/null +++ b/tests/unit/workflows/test_publish_modules_workflow.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import runpy +import textwrap +from pathlib import Path +from typing import Any, cast + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _workflow_text() -> str: + return (REPO_ROOT / ".github" / "workflows" / "publish-modules.yml").read_text(encoding="utf-8") + + +def _publish_python_block() -> str: + workflow = _workflow_text() + start = workflow.index(" import hashlib") + end = workflow.index("\n PY", start) + return textwrap.dedent(workflow[start:end]) + + +def _signature_sidecar_globals(tmp_path: Path) -> dict[str, object]: + block = _publish_python_block() + start = block.index("def write_registry_signature_sidecar") + end = block.index("\nskipped_bundles", start) + source = "\n".join( + [ + "from pathlib import Path", + f"repo_root = Path({str(tmp_path)!r})", + 'registry_signatures_dir = repo_root / "registry" / "signatures"', + "registry_signatures_dir.mkdir(parents=True, exist_ok=True)", + block[start:end], + "skipped_bundles = {'specfact-example': '1.2.3'}", + ] + ) + script_path = tmp_path / "isolated_publish_module_sidecar.py" + script_path.write_text(source, encoding="utf-8") + return runpy.run_path(str(script_path)) + + +def test_publish_modules_writes_signature_sidecars_for_skipped_bundles() -> None: + workflow = _workflow_text() + + assert "def write_registry_signature_sidecar(" in workflow + assert 'signature_path = registry_signatures_dir / f"{bundle}-{version}.tar.sig"' in workflow + assert "for bundle, published_version in skipped_bundles.items():" in workflow + assert "write_registry_signature_sidecar(bundle, manifest, expected_version=published_version)" in workflow + + +def test_publish_modules_skipped_signature_sidecar_requires_published_version(tmp_path: Path) -> None: + namespace = _signature_sidecar_globals(tmp_path) + write_registry_signature_sidecar = cast(Any, namespace["write_registry_signature_sidecar"]) + registry_signatures_dir = cast(Path, namespace["registry_signatures_dir"]) + skipped_bundles = cast(dict[str, str], namespace["skipped_bundles"]) + + bundle = "specfact-example" + version = skipped_bundles[bundle] + write_registry_signature_sidecar( + bundle, + {"version": version, "integrity": {"signature": "signed"}}, + expected_version=version, + ) + sidecar_path = registry_signatures_dir / f"{bundle}-{version}.tar.sig" + assert sidecar_path.read_text(encoding="utf-8") == "signed\n" + + write_registry_signature_sidecar( + "specfact-other", + {"version": "9.9.9", "integrity": {"signature": "wrong"}}, + expected_version=version, + ) + assert not (registry_signatures_dir / "specfact-other-9.9.9.tar.sig").exists() + + +def test_publish_modules_tracks_registry_signatures_in_publish_commit() -> None: + workflow = _workflow_text() + + assert "git diff --quiet -- registry/index.json registry/modules registry/signatures" in workflow + assert "git add registry/index.json registry/modules registry/signatures" in workflow