Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ All notable changes to this project will be documented in this file.

---

## [0.47.7] - 2026-06-12

### Fixed

- **Stale flat command references**: update runtime error suggestions and
tooling references from removed flat commands (`specfact analyze`,
`specfact repro`, `specfact sync bridge`, `specfact enforce sdd`) to the
canonical grouped commands (`specfact code analyze contracts`,
`specfact code repro`, `specfact project sync bridge`,
`specfact govern enforce sdd`).

### Added

- **llms.txt freshness test**: add a unit test that re-runs the command
overview generator in check mode so a stale `llms.txt` or generated command
reference fails the test suite even when the path-scoped pre-commit gate is
bypassed.

## [0.47.6] - 2026-06-12

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "specfact-cli"
version = "0.47.6"
version = "0.47.7"
description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
6 changes: 3 additions & 3 deletions scripts/export-change-to-github.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Export OpenSpec change proposals to GitHub issues via specfact sync bridge.
"""Export OpenSpec change proposals to GitHub issues via specfact project sync bridge.

This wrapper standardizes the common OpenSpec->GitHub export command and adds a
friendly `--inplace-update` option that maps to `--update-existing`.
Expand Down Expand Up @@ -30,7 +30,7 @@ def build_export_command(
repo_name: str | None,
inplace_update: bool,
) -> list[str]:
"""Build `specfact sync bridge` command for GitHub export."""
"""Build `specfact project sync bridge` command for GitHub export."""
cleaned_ids = [item.strip() for item in change_ids if item.strip()]
if not cleaned_ids:
raise ViolationError("At least one non-empty change id is required")
Expand Down Expand Up @@ -77,7 +77,7 @@ def main(argv: list[str] | None = None) -> int:
"""CLI entrypoint."""
parser = argparse.ArgumentParser(
description=(
"Export OpenSpec change proposal(s) to GitHub via `specfact sync bridge` "
"Export OpenSpec change proposal(s) to GitHub via `specfact project sync bridge` "
"with optional in-place issue update."
)
)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
if __name__ == "__main__":
_setup = setup(
name="specfact-cli",
version="0.47.6",
version="0.47.7",
description=(
"AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, "
"and spec/contract evidence for AI-assisted and brownfield delivery."
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"""

# Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
__version__ = "0.47.6"
__version__ = "0.47.7"
2 changes: 1 addition & 1 deletion src/specfact_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None:
# keeps missing-command and missing-parameter UX consistent outside the root CLI too.
_install_progressive_disclosure()

__version__ = "0.47.6"
__version__ = "0.47.7"

__all__ = ["__version__"]
8 changes: 4 additions & 4 deletions src/specfact_cli/utils/suggestions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def suggest_next_steps(repo_path: Path, context: ProjectContext | None = None) -

# Analysis suggestions
if context.has_plan and context.contract_coverage < 0.5:
suggestions.append("specfact analyze --bundle <name> # Analyze contract coverage")
suggestions.append("specfact code analyze contracts --bundle <name> # Analyze contract coverage")
suggestions.append("specfact code import --repo . <name> # Update the project bundle from code")

# Specmatic integration suggestions
Expand Down Expand Up @@ -95,8 +95,8 @@ def suggest_fixes(error_message: str, context: ProjectContext | None = None) ->

# Contract validation errors
if "contract" in error_lower and ("violation" in error_lower or "invalid" in error_lower):
suggestions.append("specfact analyze --bundle <name> # Analyze contract violations")
suggestions.append("specfact repro --bundle <name> # Run validation suite")
suggestions.append("specfact code analyze contracts --bundle <name> # Analyze contract violations")
suggestions.append("specfact code repro --repo . # Run validation suite")

# Specmatic errors
if "specmatic" in error_lower or "openapi" in error_lower:
Expand Down Expand Up @@ -126,7 +126,7 @@ def suggest_improvements(context: ProjectContext) -> list[str]:

# Low contract coverage
if context.contract_coverage < 0.3:
suggestions.append("specfact analyze --bundle <name> # Identify missing contracts")
suggestions.append("specfact code analyze contracts --bundle <name> # Identify missing contracts")
suggestions.append("specfact code import --repo . <name> # Extract contracts from code")

# Missing OpenAPI specs
Expand Down
71 changes: 71 additions & 0 deletions tests/unit/docs/test_llms_overview_freshness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Guard against stale generated command artifacts (llms.txt and command reference).

The pre-commit command-overview gate only fires when specific paths are staged, so a
commit that bypasses it (merge commits, --no-verify, bot commits) can land a stale
llms.txt. A stale llms.txt misleads agents worse than a missing one, so this test
re-runs the generator in --check mode on every test run.
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parents[3]
GENERATOR = REPO_ROOT / "scripts" / "generate-command-overview.py"
GENERATED_ARTIFACTS = (
"llms.txt",
"docs/reference/commands.generated.json",
"docs/reference/commands.generated.md",
)


def _paired_worktree_modules_repo() -> Path | None:
"""Mirror the generator's paired-worktree candidate (specfact-cli-worktrees layout)."""
parts = REPO_ROOT.parts
if "specfact-cli-worktrees" not in parts:
return None
marker_index = parts.index("specfact-cli-worktrees")
base = Path(*parts[:marker_index])
suffix = Path(*parts[marker_index + 1 :])
return base / "specfact-cli-modules-worktrees" / suffix


def _modules_repo_available() -> bool:
configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip()
candidates = [
Path(configured).expanduser() if configured else None,
REPO_ROOT.parent / "specfact-cli-modules",
_paired_worktree_modules_repo(),
]
Comment thread
djm81 marked this conversation as resolved.
return any(candidate is not None and (candidate / "packages").is_dir() for candidate in candidates)


def test_generated_command_artifacts_exist() -> None:
for relative in GENERATED_ARTIFACTS:
assert (REPO_ROOT / relative).is_file(), f"Missing generated artifact: {relative}"


def test_llms_and_command_overview_are_current() -> None:
"""llms.txt and the generated command reference must match the current CLI surface."""
if not _modules_repo_available():
pytest.skip("specfact-cli-modules packages checkout not available")

result = subprocess.run(
[sys.executable, str(GENERATOR), "--check"],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
timeout=300,
)
assert result.returncode == 0, (
"Generated command artifacts (llms.txt, docs/reference/commands.generated.*) are stale. "
"Regenerate with 'hatch run generate-command-overview' and commit the result.\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
10 changes: 6 additions & 4 deletions tools/validate_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ def _prompts_dir_is_valid(prompts_dir: Path | None) -> bool:

CLI_COMMANDS = {
"specfact.01-import": "specfact code import",
# The plan verbs below stay on the legacy surface until the rehome tracked in
# nold-ai/specfact-cli-modules#317 lands; update them together with the prompts.
"specfact.02-plan": "specfact plan <operation>", # init, add-feature, add-story, update-idea, update-feature, update-story
"specfact.03-review": "specfact plan review", # Also handles promote
"specfact.04-sdd": "specfact plan harden",
"specfact.05-enforce": "specfact enforce sdd",
"specfact.06-sync": "specfact sync bridge",
"specfact.07-contracts": "specfact analyze contracts", # Also uses generate contracts-prompt and contracts-apply
"specfact.05-enforce": "specfact govern enforce sdd",
"specfact.06-sync": "specfact project sync bridge",
"specfact.07-contracts": "specfact code analyze contracts", # Also uses generate contracts-prompt and contracts-apply
"specfact.compare": "specfact plan compare",
"specfact.validate": "specfact repro",
"specfact.validate": "specfact code repro",
}

# Required CLI enforcement rules (checking for key phrases, flexible matching)
Expand Down
Loading