From 17ee3c555ffdf50c5a7294f5ad8077debc0c7431 Mon Sep 17 00:00:00 2001 From: Dominikus Nold Date: Sun, 29 Mar 2026 23:12:23 +0200 Subject: [PATCH] chore(release): v0.43.2 pre-commit review JSON + OpenSpec dogfood rules - Pre-commit gate writes ReviewReport JSON to .specfact/code-review.json - openspec/config.yaml: require fresh review JSON and remediate findings - Docs and unit tests updated Made-with: Cursor --- CHANGELOG.md | 8 ++++ docs/modules/code-review.md | 6 ++- openspec/config.yaml | 18 ++++++++ pyproject.toml | 2 +- scripts/pre_commit_code_review.py | 43 ++++++++++++++++--- setup.py | 2 +- src/__init__.py | 2 +- src/specfact_cli/__init__.py | 2 +- .../scripts/test_code_review_module_docs.py | 3 +- .../scripts/test_pre_commit_code_review.py | 16 ++++--- 10 files changed, 82 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74212e47..10e22416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. --- +## [0.43.2] - 2026-03-29 + +### Changed + +- Pre-commit `specfact-code-review-gate` now runs `specfact code review run --json --out .specfact/code-review.json` (instead of `--score-only`) so the governed `ReviewReport` JSON is written under gitignored `.specfact/` for IDE and Copilot workflows. + +--- + ## [0.43.1] - 2026-03-28 ### Changed diff --git a/docs/modules/code-review.md b/docs/modules/code-review.md index 2ea7a5cb..27369500 100644 --- a/docs/modules/code-review.md +++ b/docs/modules/code-review.md @@ -121,9 +121,11 @@ repos: The helper script scopes the gate to staged Python files only and then runs: ```bash -specfact code review run --score-only +specfact code review run --json --out .specfact/code-review.json ``` +The JSON report is written under ``.specfact/`` (ignored by git via ``.specfact/`` in ``.gitignore``) so local tools and Copilot can read structured findings. The CLI still echoes the output path on success. + Commit behavior: - `PASS` keeps the commit green @@ -148,7 +150,7 @@ repos: hooks: - id: specfact-code-review name: specfact code review gate - entry: specfact code review run --score-only + entry: specfact code review run --json --out .specfact/code-review.json language: system files: \.pyi?$ ``` diff --git a/openspec/config.yaml b/openspec/config.yaml index 524f18ed..7751788d 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -61,6 +61,11 @@ context: | `openspec/changes//TDD_EVIDENCE.md` for every behavior change. - If the pattern does not work in practice, adjust the process until it does. + Code review JSON (dogfood): Treat `.specfact/code-review.json` as mandatory evidence before an OpenSpec + change is complete. If the file is missing or stale (see tasks), run + `specfact code review run --json --out .specfact/code-review.json` and resolve **every** finding at any + severity (warning or error) unless the proposal documents a rare, explicit exception. + # Per-artifact rules (only injected into matching artifacts) rules: proposal: @@ -158,3 +163,16 @@ rules: - body following `.github/ISSUE_TEMPLATE/change_proposal.md` template (Why, What Changes sections from proposal) - footer `*OpenSpec Change Proposal: *` - After creation, update proposal.md Source Tracking section with issue number, URL, repository, and status. + - |- + SpecFact code review JSON (dogfood, required before PR): Include tasks to + - (1) Ensure `.specfact/code-review.json` is present and **fresh**: if the file is missing, or its + last-modified time is older than any file you changed in this change under `src/`, `scripts/`, + `tools/`, `tests/`, or `openspec/changes//`, run a new review, e.g. + `hatch run specfact code review run --json --out .specfact/code-review.json` with `--scope changed` + during iteration and `--scope full` (or equivalent coverage) before the final PR. + - (2) Read the JSON report and remediate **all** findings regardless of severity (warning, advisory, + error, or equivalent in the schema): treat them as blocking until fixed or the proposal explicitly + documents a justified exception approved in the change. + - (3) Re-run the review after substantive edits until the report shows a passing outcome per the + review module (e.g. overall verdict PASS / CI exit 0); record the review command(s) and timestamp + in `TDD_EVIDENCE.md` or the PR description when the change touches behavior or quality gates. diff --git a/pyproject.toml b/pyproject.toml index 4cb884b4..36492f7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "specfact-cli" -version = "0.43.1" +version = "0.43.2" description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases." readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/pre_commit_code_review.py b/scripts/pre_commit_code_review.py index 859fafda..6b5134e0 100644 --- a/scripts/pre_commit_code_review.py +++ b/scripts/pre_commit_code_review.py @@ -1,4 +1,8 @@ -"""Run specfact code review as a staged-file pre-commit gate.""" +"""Run specfact code review as a staged-file pre-commit gate. + +Writes a machine-readable JSON report to ``.specfact/code-review.json`` (gitignored) +so IDEs and Copilot can read findings; exit code still reflects the governed CI verdict. +""" # CrossHair: ignore # This helper shells out to the CLI and is intentionally side-effecting. @@ -16,6 +20,9 @@ PYTHON_SUFFIXES = {".py", ".pyi"} +# Default matches dogfood / OpenSpec: machine-readable report under ignored ``.specfact/``. +REVIEW_JSON_OUT = ".specfact/code-review.json" + @require(lambda paths: paths is not None) @ensure(lambda result: len(result) == len(set(result))) @@ -36,13 +43,29 @@ def filter_review_files(paths: Sequence[str]) -> list[str]: @require(lambda files: files is not None) @ensure(lambda result: result[:5] == [sys.executable, "-m", "specfact_cli.cli", "code", "review"]) -@ensure(lambda result: "--score-only" in result) +@ensure(lambda result: "--json" in result and "--out" in result) +@ensure(lambda result: REVIEW_JSON_OUT in result) def build_review_command(files: Sequence[str]) -> list[str]: - """Build the score-only review command used by the pre-commit gate.""" - return [sys.executable, "-m", "specfact_cli.cli", "code", "review", "run", "--score-only", *files] + """Build ``code review run --json --out …`` so findings are written for tooling.""" + return [ + sys.executable, + "-m", + "specfact_cli.cli", + "code", + "review", + "run", + "--json", + "--out", + REVIEW_JSON_OUT, + *files, + ] + + +def _repo_root() -> Path: + """Repository root (parent of ``scripts/``).""" + return Path(__file__).resolve().parents[1] -@ensure(lambda result: isinstance(result[0], bool)) def ensure_runtime_available() -> tuple[bool, str | None]: """Verify the current Python environment can import SpecFact CLI.""" try: @@ -54,7 +77,7 @@ def ensure_runtime_available() -> tuple[bool, str | None]: @ensure(lambda result: isinstance(result, int)) def main(argv: Sequence[str] | None = None) -> int: - """Run the score-only review gate over staged Python files.""" + """Run the code review gate; write JSON under ``.specfact/`` and return CLI exit code.""" files = filter_review_files(list(argv or [])) if not files: sys.stdout.write("No staged Python files to review; skipping code review gate.\n") @@ -65,7 +88,13 @@ def main(argv: Sequence[str] | None = None) -> int: sys.stdout.write(f"Unable to run the code review gate. {guidance}\n") return 1 - result = subprocess.run(build_review_command(files), check=False, text=True, capture_output=True) + result = subprocess.run( + build_review_command(files), + check=False, + text=True, + capture_output=True, + cwd=str(_repo_root()), + ) if result.stdout: sys.stdout.write(result.stdout) if result.stderr: diff --git a/setup.py b/setup.py index f5df4c72..525f4944 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ if __name__ == "__main__": _setup = setup( name="specfact-cli", - version="0.43.1", + version="0.43.2", description=( "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with " "validation and contract enforcement for new projects and long-lived codebases." diff --git a/src/__init__.py b/src/__init__.py index d44458c3..9678e8e8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,4 +3,4 @@ """ # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py -__version__ = "0.43.1" +__version__ = "0.43.2" diff --git a/src/specfact_cli/__init__.py b/src/specfact_cli/__init__.py index ef990799..7037ecc0 100644 --- a/src/specfact_cli/__init__.py +++ b/src/specfact_cli/__init__.py @@ -42,6 +42,6 @@ def _bootstrap_bundle_paths() -> None: _bootstrap_bundle_paths() -__version__ = "0.43.1" +__version__ = "0.43.2" __all__ = ["__version__"] diff --git a/tests/unit/scripts/test_code_review_module_docs.py b/tests/unit/scripts/test_code_review_module_docs.py index d2b90f6e..8617f949 100644 --- a/tests/unit/scripts/test_code_review_module_docs.py +++ b/tests/unit/scripts/test_code_review_module_docs.py @@ -11,7 +11,8 @@ def test_code_review_docs_cover_pre_commit_gate_and_portable_adoption() -> None: docs = _docs_text() assert "## Pre-Commit Review Gate" in docs assert ".pre-commit-config.yaml" in docs - assert "specfact code review run --score-only" in docs + assert "specfact code review run" in docs + assert ".specfact/code-review.json" in docs assert "## Add to Any Project" in docs diff --git a/tests/unit/scripts/test_pre_commit_code_review.py b/tests/unit/scripts/test_pre_commit_code_review.py index 03ec8c38..304696bc 100644 --- a/tests/unit/scripts/test_pre_commit_code_review.py +++ b/tests/unit/scripts/test_pre_commit_code_review.py @@ -34,14 +34,16 @@ def test_filter_review_files_keeps_only_python_sources() -> None: ] -def test_build_review_command_uses_score_only_mode() -> None: - """Pre-commit gate should rely on score-only exit-code semantics.""" +def test_build_review_command_writes_json_report() -> None: + """Pre-commit gate should write ReviewReport JSON for IDE/Copilot and use exit verdict.""" module = _load_script_module() command = module.build_review_command(["src/app.py", "tests/test_app.py"]) assert command[:5] == [sys.executable, "-m", "specfact_cli.cli", "code", "review"] - assert "--score-only" in command + assert "--json" in command + assert "--out" in command + assert module.REVIEW_JSON_OUT in command assert command[-2:] == ["src/app.py", "tests/test_app.py"] @@ -62,9 +64,11 @@ def test_main_propagates_review_gate_exit_code(monkeypatch: pytest.MonkeyPatch) def _fake_ensure() -> tuple[bool, str | None]: return True, None - def _fake_run(cmd: list[str], **_: object) -> subprocess.CompletedProcess[str]: - assert "--score-only" in cmd - return subprocess.CompletedProcess(cmd, 1, stdout="-7\n", stderr="") + def _fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + assert "--json" in cmd + assert module.REVIEW_JSON_OUT in cmd + assert kwargs.get("cwd") is not None + return subprocess.CompletedProcess(cmd, 1, stdout=".specfact/code-review.json\n", stderr="") monkeypatch.setattr(module, "ensure_runtime_available", _fake_ensure) monkeypatch.setattr(module.subprocess, "run", _fake_run)