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
4 changes: 3 additions & 1 deletion .github/workflows/pr-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ jobs:
if-no-files-found: ignore

linting:
name: Linting (ruff, pylint)
name: Linting (ruff, pylint, safe-write guard)
runs-on: ubuntu-latest
needs: [changes, verify-module-signatures]
if: needs.changes.outputs.code_changed == 'true' && needs.changes.outputs.skip_tests_dev_to_main != 'true'
Expand Down Expand Up @@ -545,10 +545,12 @@ jobs:
mkdir -p logs/lint
LINT_LOG="logs/lint/lint_$(date -u +%Y%m%d_%H%M%S).log"
{
set -euo pipefail
ruff format . --check
python -m basedpyright --pythonpath "$(python -c 'import sys; print(sys.executable)')"
ruff check .
pylint src tests tools
python scripts/verify_safe_project_writes.py
} 2>&1 | tee "$LINT_LOG"
exit "${PIPESTATUS[0]:-$?}"
- name: Upload lint logs
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ All notable changes to this project will be documented in this file.

---

## [0.45.2] - 2026-04-12

### Fixed

- **`specfact init ide` and `.vscode/settings.json`**: invalid JSON or non-mergeable `chat` blocks no longer
wipe unrelated VS Code settings; the command fails safe with guidance. Use `--force` only when you accept
replacing the file after a timestamped backup under `.specfact/recovery/`.
- **VS Code settings path**: resolved settings paths must stay inside the repository root (blocks symlink
escape); settings are parsed with **JSON5** so JSONC-style comments and trailing commas load correctly.
Serialized output is canonical JSON (comments from the original file are not preserved on rewrite).
- **`create_vscode_settings`**: an explicit empty `prompts_by_source` mapping no longer falls back to the
full prompt catalog when finalizing recommendations.
- **Regression gate**: lint now runs `scripts/verify_safe_project_writes.py` so IDE settings JSON I/O stays
routed through the shared merge helper.
- **Dev / Semgrep**: Hatch and `[dev]` extras pin `setuptools<82` so Semgrep’s OpenTelemetry import chain still
resolves `pkg_resources` (setuptools 82+ may omit it).

---

## [0.45.1] - 2026-04-03

### Changed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ uvx specfact-cli code review run --path . --scope full
**Sample output:**

```text
SpecFact CLI - v0.45.1
SpecFact CLI - v0.45.2

Running Ruff checks...
Running Radon complexity checks...
Expand Down Expand Up @@ -84,7 +84,7 @@ It exists because delivery drifts in predictable ways:

```yaml
- repo: https://github.com/nold-ai/specfact-cli
rev: v0.45.1
rev: v0.45.2
hooks:
- id: specfact-smart-checks
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

CLI_VERSION="${CLI_VERSION:-0.45.1}"
CLI_VERSION="${CLI_VERSION:-0.45.2}"
REPO_SLUG="${REPO_SLUG:-nold-ai/specfact-demo-repo}"
CAPTURE_REF="${CAPTURE_REF:-${CAPTURE_COMMIT:-2b5ba8cd57d16c1a1f24463a297fdb28fbede123}}"
WORK_DIR="${WORK_DIR:-/tmp/specfact-demo-repo}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# README sample output capture

- CLI version: `0.45.1`
- CLI version: `0.45.2`
- Repo: `nold-ai/specfact-demo-repo`
- Repo ref: `2b5ba8cd57d16c1a1f24463a297fdb28fbede123`
- Review exit code: `1`
Expand All @@ -9,8 +9,8 @@
- Command:

```bash
uvx --from "specfact-cli==0.45.1" specfact init --profile solo-developer
uvx --from "specfact-cli==0.45.1" --with ruff --with radon --with semgrep --with basedpyright --with pylint --with crosshair-tool specfact code review run --path . --scope full
uvx --from "specfact-cli==0.45.2" specfact init --profile solo-developer
uvx --from "specfact-cli==0.45.2" --with ruff --with radon --with semgrep --with basedpyright --with pylint --with crosshair-tool specfact code review run --path . --scope full
```

- Raw output: `/workspace/docs/_support/readme-first-contact/sample-output/review-output.txt`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Downloading z3-solver (30.3MiB)
Downloaded basedpyright
Downloaded nodejs-wheel-binaries
Installed 111 packages in 394ms
SpecFact CLI - v0.45.1
SpecFact CLI - v0.45.2


⏱️ Started: 2026-04-03 20:55:28
Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ specfact init ide --ide cursor --install-deps

**Important**: SpecFact CLI does **not** ship with built-in AI. `specfact init ide` installs prompt templates for supported IDEs so your chosen AI copilot can call SpecFact commands in a guided workflow.

For VS Code / Copilot, the CLI **merges** prompt recommendations into `.vscode/settings.json` and keeps your other
settings keys. If that file is not valid JSON (or its `chat` block is not mergeable), the command stops without
rewriting it; use `specfact init ide --force` only when you accept replacing the file after a timestamped backup under
`.specfact/recovery/`.

[More options ↓](#more-options)

## More options
Expand Down
6 changes: 3 additions & 3 deletions modules/bundle-mapper/module-package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: bundle-mapper
version: 0.1.7
version: 0.1.8
commands: []
category: core
pip_dependencies: []
Expand All @@ -20,8 +20,8 @@ publisher:
url: https://github.com/nold-ai/specfact-cli-modules
email: hello@noldai.com
integrity:
checksum: sha256:6b078e7855d9acd3ce9abf0464cdab7f22753dd2ce4b5fc7af111ef72bc50f02
signature: v6/kVxxR/CNNnXkS2TTgeEAKPFS5ErPRf/GbwM0U9H20txu9kwZb6r5rQP9Spu5EZ+IdTs4JJ9cInicPwmE1Bw==
checksum: sha256:b52ab14496e35b5d4e8da7bbbd8573eacc909bf50e365f9948d8bb0b8e174ae1
signature: SXjIFMZSrXD7jQ4MHpD39tKyNJVHWF1P25swAJdDzaEkQd9A+llFSWvz1v2RmkanIej+XG7uZszokIBzwscbCQ==
dependencies: []
description: Map backlog items to best-fit modules using scoring heuristics.
license: Apache-2.0
54 changes: 33 additions & 21 deletions modules/bundle-mapper/src/bundle_mapper/mapper/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, cast

Expand All @@ -30,6 +31,14 @@
HISTORY_CAP = 10.0


@dataclass(frozen=True, slots=True)
class _SignalContribution:
bundle_id: str
score: float
weight: float
source: str


def _tokenize(text: str) -> set[str]:
"""Lowercase, split by non-alphanumeric."""
return set(re.findall(r"[a-z0-9]+", text.lower()))
Expand Down Expand Up @@ -154,19 +163,16 @@ def _apply_signal_contribution(
primary_bundle_id: str | None,
weighted: float,
reasons: list[str],
bundle_id: str,
score: float,
weight: float,
source: str,
signal: _SignalContribution,
) -> tuple[str | None, float]:
"""Apply one signal contribution to the primary score."""
if bundle_id and score > 0:
contrib = weight * score
if signal.bundle_id and signal.score > 0:
contrib = signal.weight * signal.score
if primary_bundle_id is None:
primary_bundle_id = bundle_id
primary_bundle_id = signal.bundle_id
weighted += contrib
reasons.append(self._explain_score(bundle_id, score, source))
elif bundle_id == primary_bundle_id:
reasons.append(self._explain_score(signal.bundle_id, signal.score, signal.source))
elif signal.bundle_id == primary_bundle_id:
weighted += contrib
return primary_bundle_id, weighted

Expand Down Expand Up @@ -210,19 +216,23 @@ def compute_mapping(self, item: BacklogItem) -> BundleMapping:
primary_bundle_id,
weighted,
reasons,
explicit_bundle or "",
explicit_score,
WEIGHT_EXPLICIT,
"explicit_label",
_SignalContribution(
bundle_id=explicit_bundle or "",
score=explicit_score,
weight=WEIGHT_EXPLICIT,
source="explicit_label",
),
)
primary_bundle_id, weighted = self._apply_signal_contribution(
primary_bundle_id,
weighted,
reasons,
hist_bundle or "",
hist_score,
WEIGHT_HISTORICAL,
"historical",
_SignalContribution(
bundle_id=hist_bundle or "",
score=hist_score,
weight=WEIGHT_HISTORICAL,
source="historical",
),
)

if content_list:
Expand All @@ -231,10 +241,12 @@ def compute_mapping(self, item: BacklogItem) -> BundleMapping:
primary_bundle_id,
weighted,
reasons,
best_content_bundle,
best_content_score,
WEIGHT_CONTENT,
"content_similarity",
_SignalContribution(
bundle_id=best_content_bundle,
score=best_content_score,
weight=WEIGHT_CONTENT,
source="content_similarity",
),
)

confidence = min(1.0, weighted)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# TDD evidence β€” profile-04-safe-project-artifact-writes

## Failing-first (targeted)

- **When**: 2026-04-12 (Europe/Berlin)
- **Command**:

```bash
cd ../specfact-cli-worktrees/bugfix/profile-04-safe-project-artifact-writes
hatch run pytest \
tests/unit/utils/test_project_artifact_write.py \
tests/unit/utils/test_ide_setup.py \
tests/unit/scripts/test_verify_safe_project_writes.py \
tests/unit/modules/init/test_init_ide_prompt_selection.py \
-q
```

- **Note**: New scenarios (`malformed_json_raises`, `preserves_unrelated_keys`, verify script) were added
before the safe-merge implementation; prior behavior treated invalid JSON as `{}` and could destroy user
settings (issue #487).

## Passing-after (targeted + e2e)

- **When**: 2026-04-12
- **Commands**:

```bash
hatch run pytest tests/unit/utils/test_project_artifact_write.py \
tests/unit/utils/test_ide_setup.py tests/unit/scripts/test_verify_safe_project_writes.py \
tests/unit/modules/init/test_init_ide_prompt_selection.py tests/e2e/test_init_command.py -q
hatch run format && hatch run type-check && hatch run lint
hatch run contract-test
hatch run smart-test
hatch test --cover -v
```
Comment thread
djm81 marked this conversation as resolved.

- **Full suite + coverage (`tasks.md` 4.3)**: same worktree; `hatch test --cover -v` β€” **exit 0**. Pytest summary:
`2450 passed, 9 skipped in 358.61s (0:05:58)`. Coverage footer (pytest-cov): `TOTAL ... 62%` on the combined
`src/` + `tools/` table (see run log for per-file lines).

- **Module signatures**: run
`hatch run ./scripts/verify-modules-signature.py --require-signature` β€” pass without bumping
`src/specfact_cli/modules/init/module-package.yaml` (init UX errors are raised from `ide_setup` so the init
module payload checksum is unchanged).

## Code review gate

- **Pass (2026-04-12)**: after `hatch run specfact module install nold-ai/specfact-codebase` and
`hatch run specfact module install nold-ai/specfact-code-review` (user scope), run:

```bash
hatch run specfact code review run --json --out .specfact/code-review.json \
src/specfact_cli/utils/project_artifact_write.py \
src/specfact_cli/utils/ide_setup.py \
scripts/verify_safe_project_writes.py
```

- Report: `.specfact/code-review.json` (exit 0, no blocking findings after merge-helper refactor and setuptools
pin).

## OpenSpec strict validation

- **Pass (2026-04-12)**:
`openspec validate profile-04-safe-project-artifact-writes --strict` β€” exit 0 (recorded at sign-off per
`tasks.md` 4.7).

## Worktree cleanup (post-merge on developer machine)

- Remove worktree, delete branch, prune β€” see `tasks.md` section 5 (not executed in this implementation
session).
65 changes: 47 additions & 18 deletions openspec/changes/profile-04-safe-project-artifact-writes/tasks.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,64 @@
## 1. Branch, coordination, and issue sync

- [ ] 1.1 Create `bugfix/profile-04-safe-project-artifact-writes` in a dedicated worktree from `origin/dev` and bootstrap Hatch in that worktree.
- [ ] 1.2 Sync the change proposal to GitHub under parent feature `#365`, link bug `#487`, and update `proposal.md` Source Tracking with issue metadata.
- [ ] 1.3 Confirm the paired modules-side change `project-runtime-01-safe-artifact-write-policy` is available and note the dependency in both PR descriptions/change evidence.
- [x] 1.1 Create `bugfix/profile-04-safe-project-artifact-writes` in a dedicated worktree from
`origin/dev`; run `hatch env create`, then pre-flight status checks `hatch run smart-test-status` and
`hatch run contract-test-status`.
- [ ] 1.2 Sync the change proposal to GitHub under parent feature `#365`, link bug `#487`, and update
`proposal.md` Source Tracking with issue metadata. *(human / PR author)*
- [ ] 1.3 Confirm the paired modules-side change `project-runtime-01-safe-artifact-write-policy` is
available and note the dependency in both PR descriptions/change evidence. *(human)*

## 2. Specs, regression fixtures, and failing evidence

- [ ] 2.1 Add or update regression fixtures for existing user-owned project artifacts such as `.vscode/settings.json` with unrelated custom settings.
- [ ] 2.2 Write tests from the new scenarios covering partial ownership, malformed settings fail-safe behavior, backup creation, and preservation of unrelated settings.
- [ ] 2.3 Write tests for the CI/static unsafe-write gate so direct writes to protected project artifacts are rejected unless routed through the sanctioned helper.
- [ ] 2.4 Run the targeted tests before implementation, capture the failing results, and record commands/timestamps in `openspec/changes/profile-04-safe-project-artifact-writes/TDD_EVIDENCE.md`.
- [x] 2.1 Add or update regression fixtures for existing user-owned project artifacts such as
`.vscode/settings.json` with unrelated custom settings. *(pytest tmp_path fixtures in
`test_project_artifact_write.py`)*
- [x] 2.2 Write tests from the new scenarios covering partial ownership, malformed settings fail-safe
behavior, backup creation, and preservation of unrelated settings.
- [x] 2.3 Write tests for the CI/static unsafe-write gate so direct writes to protected project
artifacts are rejected unless routed through the sanctioned helper.
- [x] 2.4 Run the targeted tests before implementation, capture the failing results, and record
commands/timestamps in `openspec/changes/profile-04-safe-project-artifact-writes/TDD_EVIDENCE.md`.

## 3. Core safe-write implementation

- [ ] 3.1 Implement the core safe-write helper and ownership model with `@beartype` and `@icontract` on public APIs.
- [ ] 3.2 Route `src/specfact_cli/utils/ide_setup.py` settings mutation through the helper so `.vscode/settings.json` preserves unrelated user-managed settings and strips only SpecFact-managed entries when needed.
- [ ] 3.3 Route applicable init/setup artifact copy flows through the helper or explicit safe modes, including fail-safe handling for malformed structured files and backup creation for explicit replacement.
- [ ] 3.4 Implement the CI/static guard for protected user-project artifacts in init/setup code paths and integrate it into the relevant local/CI quality workflow.
- [x] 3.1 Implement the core safe-write helper and ownership model with `@beartype` and `@icontract`
on public APIs.
- [x] 3.2 Route `src/specfact_cli/utils/ide_setup.py` settings mutation through the helper so
`.vscode/settings.json` preserves unrelated user-managed settings and strips only SpecFact-managed
entries when needed.
- [x] 3.3 Route applicable init/setup artifact copy flows through the helper or explicit safe modes,
including fail-safe handling for malformed structured files and backup creation for explicit
replacement. *(VS Code settings path; `init ide` surfaces errors + `--force` + backup)*
- [x] 3.4 Implement the CI/static guard for protected user-project artifacts in init/setup code paths
and integrate it into the relevant local/CI quality workflow. *(`scripts/verify_safe_project_writes.py`
+ `hatch run lint`)*

## 4. Verification, docs, and cross-repo handoff

- [ ] 4.1 Re-run the targeted tests and any broader init/setup regression coverage, capture passing results, and update `TDD_EVIDENCE.md`.
- [ ] 4.2 Research and update affected docs (`README.md`, installation/quickstart/init references) to document preservation guarantees, backup behavior, and explicit replacement semantics.
- [ ] 4.3 Run quality gates: `hatch run format`, `hatch run type-check`, `hatch run lint`, `hatch run yaml-lint`, `hatch run contract-test`, and `hatch run smart-test`.
- [ ] 4.4 Run `hatch run ./scripts/verify-modules-signature.py --require-signature`; if any bundled module manifests changed, bump versions, re-sign as required, and re-run verification.
- [ ] 4.5 Ensure `.specfact/code-review.json` is fresh, remediate all findings, and record the final review command/timestamp in `TDD_EVIDENCE.md` or PR notes.
- [ ] 4.6 Apply the appropriate version/changelog update for a bugfix release if implementation changes user-facing behavior, then open a PR to `dev` referencing the paired modules change.
- [x] 4.1 Re-run the targeted tests and any broader init/setup regression coverage, capture passing
results, and update `TDD_EVIDENCE.md`.
- [x] 4.2 Research and update affected docs (`README.md`, installation/quickstart/init references) to
document preservation guarantees, backup behavior, and explicit replacement semantics.
*(installation.md + version pins in README / samples)*
- [x] 4.3 Run quality gates: `hatch run format`, `hatch run type-check`, `hatch run lint`,
`hatch run yaml-lint`, `hatch run contract-test`, `hatch run smart-test`, and `hatch test --cover -v`.
- [x] 4.4 Run `hatch run ./scripts/verify-modules-signature.py --require-signature`; if any bundled
Comment thread
coderabbitai[bot] marked this conversation as resolved.
module manifests changed, bump versions, re-sign as required, and re-run verification. *(no
`modules/init` payload change β€” error UX handled in `ide_setup` to avoid re-signing)*
- [x] 4.5 Ensure `.specfact/code-review.json` is fresh, remediate all findings, and record the final
review command/timestamp in `TDD_EVIDENCE.md` or PR notes.
- [x] 4.6 Apply the appropriate version/changelog update for a bugfix release if implementation changes
user-facing behavior, then open a PR to `dev` referencing the paired modules change.
*(version/changelog done; PR human)*
- [x] 4.7 Run strict OpenSpec validation before sign-off:
`openspec validate profile-04-safe-project-artifact-writes --strict`; fix any validation errors until
it passes; record the successful run command and timestamp in `TDD_EVIDENCE.md` (or PR notes).

## 5. Worktree cleanup

- [ ] 5.1 Remove the worktree used for this change (for example `git worktree remove ../specfact-cli-worktrees/bugfix/profile-04-safe-project-artifact-writes`).
- [ ] 5.1 Remove the worktree used for this change (for example
`git worktree remove ../specfact-cli-worktrees/bugfix/profile-04-safe-project-artifact-writes`).
- [ ] 5.2 Delete the local branch after merge (`git branch -d bugfix/profile-04-safe-project-artifact-writes`).
- [ ] 5.3 Prune stale worktree metadata (`git worktree prune`).
- [ ] 5.4 Record cleanup completion in `TDD_EVIDENCE.md` alongside the 4.x verification notes.
Loading
Loading