Skip to content

build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12 - #95

Merged
wjduenow merged 3 commits into
devfrom
feature/uv-migration
May 19, 2026
Merged

build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12#95
wjduenow merged 3 commits into
devfrom
feature/uv-migration

Conversation

@wjduenow

@wjduenow wjduenow commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Mirrors clauditor's build setup as the first of two PRs (PR B will add mkdocs / GitHub Pages docs publishing on top of this).

  • uv as canonical dev install: uv sync --dev; uv.lock is committed; CI runs every step under uv run.
  • pip install -e ".[dev]" retained for back-compat: [project.optional-dependencies].dev is kept in sync with the new [dependency-groups].dev (PEP 735). The only delta is build>=1.2,<2 in the uv group for the wheel_smoke marker.
  • 2-Python matrix (3.11 / 3.12): ruff + pytest run on both; pyright gated to the matrix floor (3.11); codecov upload gated to the ceiling (3.12).
    • 3.13 is deferred. The intended target was 3.11 / 3.12 / 3.13 — Python 3.13 changed Path.resolve() to raise OSError(errno.ELOOP) instead of RuntimeError on cyclic symlinks; six tests under tests/_common/, tests/diff/, tests/grade/, tests/manifest/ rely on the 3.11/3.12 shape. Filed as Python 3.13 compatibility for path-safety layer #96.
  • astral-sh/setup-uv@08807647… # v8.1.0 SHA-pinned per ci-supply-chain.md.
  • publish.yml: python -m builduv build in both publish-testpypi and publish-pypi jobs.

Files touched

  • pyproject.toml, uv.lock (new), .gitignore (drop stale uv.lock ignore)
  • .github/workflows/ci.yml, .github/workflows/publish.yml
  • CLAUDE.md, CONTRIBUTING.md, README.md
  • docs/cli-ops.md, docs/codecov-ops.md, docs/manifest-loader-ops.md
  • .claude/rules/python-build.md (uv-managed dev env section; refresh issue-scaffolding: reconcile pyright pythonVersion with requires-python #46 floor-pinning for the matrix; note 3.13 deferral)
  • .claude/rules/ci-supply-chain.md (add setup-uv to SHA-pin examples; graduate DEC-003 single-Python rule; note 3.13 deferral)

Rule updates

  • python-build.md § "Editable install (zsh-safe)" → § "uv-managed dev environment (uv migration)"; § "Python version" updated to describe the matrix with floor-pinning of pyright.
  • ci-supply-chain.md § "Single Python version for early milestones" → § "Python matrix: 3.11 / 3.12 (uv migration)".

Test plan

  • uv sync --dev clean from scratch on a wiped .venv/
  • uv run ruff check . clean
  • uv run ruff format --check . clean
  • uv run pyright — 0 errors / 0 warnings
  • uv run pytest — 1823 passed locally on 3.13 (6 known failures gated by Python 3.13 compatibility for path-safety layer #96 — they pass on 3.11/3.12)
  • uv run --python 3.11 pytest --no-cov — 1830/1830 pass
  • uv run --python 3.12 pytest --no-cov — 1830/1830 pass
  • uv run pytest -m wheel_smoke --no-cov — 2/2 pass
  • CI green on 3.11 and 3.12
  • No regressions in publish.yml when a future release is cut (verified by reading the workflow; not exercised on this PR)

Follow-up

#96 — Python 3.13 compatibility for path-safety layer (gate to re-adding 3.13 to the matrix).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Chores

    • Migrated project tooling from pip to uv for dependency management and builds
    • Updated CI workflow to use uv for Python environment setup and test execution
    • Updated build workflow to use uv build
    • Changed testing matrix to Python 3.11 and 3.12
  • Documentation

    • Updated all contributor and developer documentation to reflect uv-based setup workflow
    • Added uv.lock to version control for reproducible environments across all contributors

Review Change Stack

Mirrors clauditor's build setup. uv becomes the canonical dev install
(`uv sync --dev`); `uv.lock` is committed; CI runs uv-managed steps
across a 3-Python matrix. `pip install -e ".[dev]"` is retained for
back-compat — `[project.optional-dependencies].dev` and the new
`[dependency-groups].dev` are kept in sync.

- `pyproject.toml`: add `[dependency-groups].dev` (PEP 735); only
  delta vs the pip-extra is `build>=1.2,<2` for the wheel_smoke marker.
- `.github/workflows/ci.yml`: replace setup-python + pip install with
  `astral-sh/setup-uv@v8.1.0` (SHA-pinned per ci-supply-chain.md) +
  `uv sync --dev`; wrap lint/test steps with `uv run`. Matrix runs
  ruff + pytest on all three versions; pyright gated to the matrix
  floor (3.11, matching pyright.pythonVersion); codecov upload gated
  to the matrix ceiling (3.13) so coverage doesn't double-upload.
- `.github/workflows/publish.yml`: `python -m build` → `uv build` in
  both publish-testpypi and publish-pypi jobs.
- `.gitignore`: drop `uv.lock` from the ignore list (bark scaffolding
  artifact comment is stale — the canonical lock is committed now).
- `CLAUDE.md` / `CONTRIBUTING.md` / `README.md` / `docs/cli-ops.md` /
  `docs/codecov-ops.md` / `docs/manifest-loader-ops.md`: switch
  copy-pasteable dev commands to `uv sync --dev` + `uv run …`.
- `.claude/rules/python-build.md`: replace "Editable install
  (zsh-safe)" with a uv-managed dev environment section; refresh
  issue-#46 floor-pinning to describe the matrix.
- `.claude/rules/ci-supply-chain.md`: add `astral-sh/setup-uv` to the
  SHA-pinning example list; graduate DEC-003 from single-Python to a
  3-Python matrix with the pyright / codecov gating rationale.

Local validation: `uv run ruff check .` / `uv run ruff format --check
.` / `uv run pyright` / `uv run pytest` all clean under Python 3.13;
`uv run --python 3.11 pytest --no-cov` and `--python 3.12` both
1830/1830 pass. wheel_smoke passes under `uv run`. (Six pre-existing
WSL2-only symlink-loop test failures on 3.13 unaffected; pass in GHA.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (4)
  • feature/.*
  • bug/.*
  • hotfix/.*
  • feat/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0baa5de2-21cc-4f4e-aaf7-c2eeb33698b5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR systematically migrates the project from pip-based development and CI to uv-managed tooling. The CI matrix narrows to Python 3.11/3.12 only (deferring 3.13 with documented catch sites), conditional steps gate Pyright to 3.11 and Codecov uploads to 3.12, and all documentation is updated to reflect uv workflows with pip fallbacks.

Changes

uv Tooling Migration

Layer / File(s) Summary
Project configuration foundation
pyproject.toml, .gitignore
Dev dependencies are declared in both [project.optional-dependencies] and [dependency-groups] to support uv sync --dev and fallback pip install -e ".[dev]"; uv.lock is tracked in version control to reproduce CI-resolved versions.
CI workflows and supply-chain conventions
.github/workflows/ci.yml, .github/workflows/publish.yml, .claude/rules/ci-supply-chain.md
Test matrix is limited to Python 3.11 and 3.12; actions/setup-python is replaced with astral-sh/setup-uv; Pyright runs only on 3.11 and Codecov upload is gated to 3.12; publish jobs switch to uv build; documented conventions explain matrix shape and gating rationale.
Development environment and Python version conventions
.claude/rules/python-build.md, CLAUDE.md
Wheel building flow references uv run with uvx fallback; dev setup emphasizes uv sync --dev with dependency-groups mirroring; Python version policy ties requires-python, pyright.pythonVersion, and CI matrix floors together with 3.10 vs 3.11 rationale and explicit 3.13 deferral notes.
Developer and user-facing documentation
CONTRIBUTING.md, README.md, docs/cli-ops.md, docs/codecov-ops.md, docs/manifest-loader-ops.md
Local development setup, validation commands, test execution, and fixture regeneration are updated to use uv sync --dev and uv run with documented pip fallbacks for non-uv users.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related issues

  • wjduenow/SignalForge#96: The CI changes explicitly defer Python 3.13 due to Path.resolve() behavior differences and identify catch-site updates in canonicalise_path and related modules, directly addressing the pathlib OSError issue.

Possibly related PRs

  • wjduenow/SignalForge#74: This PR's reconciliation of the minimum Python version to >=3.11 is the direct predecessor to the current PR's enforcement of that floor in requires-python, pyright.pythonVersion, and the CI matrix.
  • wjduenow/SignalForge#28: Both PRs refine the Codecov CI integration—this PR gates the upload to a single matrix entry to avoid duplicates, building on the prior PR's introduction of Codecov upload.

Poem

🐰 A hop, a skip, uv takes flight!
From pip's long lists to tools so light,
Lock files locked, three-twelve's our test,
The supply chain streamlined at its best!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12' directly and concisely summarizes the two main changes: migrating to uv tooling and adjusting the Python test matrix, matching the primary objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

CI exposed six 3.13-only test failures under tests/_common/,
tests/diff/, tests/grade/, tests/manifest/. Root cause: Python 3.13
changed Path.resolve() to raise OSError(errno.ELOOP) on cyclic
symlinks instead of RuntimeError. SignalForge's _canonicalise_path
and the manifest loader's path canonicalisation catch RuntimeError
only — on 3.13 the cycle now escapes as OSError or short-circuits
to a different typed error before the loop check fires.

Fixing it touches production code (signalforge/_common/path_safety.py
and signalforge/manifest/loader.py) plus four test fixtures, which is
substantial scope creep for a tooling-migration PR. Narrow the matrix
to 3.11 / 3.12 (both green: 1830 passed locally and in CI) and track
the 3.13 work in a follow-up issue.

- ci.yml: matrix python-version: ["3.11", "3.12"]; codecov-upload gate
  flips from 3.13 → 3.12 (matrix ceiling). Comment block records the
  reason and points at the follow-up.
- CLAUDE.md / CONTRIBUTING.md / docs/cli-ops.md / python-build.md /
  ci-supply-chain.md: every "3.11 / 3.12 / 3.13" reference now reads
  "3.11 / 3.12"; ci-supply-chain.md and python-build.md add a "3.13
  is deferred" paragraph naming the catch-site fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@wjduenow wjduenow changed the title build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12/3.13 build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12 May 19, 2026
@wjduenow
wjduenow requested a review from Copilot May 19, 2026 18:24
@wjduenow

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the repo’s Python dev/CI tooling to uv (committing uv.lock and running CI steps under uv run) and expands CI from a single interpreter to a 3.11 / 3.12 matrix, with documentation updated to match the new workflows.

Changes:

  • Switch CI to astral-sh/setup-uv + uv sync --dev, and run ruff/pyright/pytest under uv run across a 3.11/3.12 matrix (pyright on 3.11 only; codecov upload on 3.12 only).
  • Add [dependency-groups].dev (PEP 735) as the uv-native dev dependency source of truth, retaining [project.optional-dependencies].dev for pip install -e ".[dev]" back-compat.
  • Update publish workflow to build via uv build, and refresh contributor/docs guidance to prefer uv.

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates install instructions to prefer uv sync --dev with a pip back-compat note.
pyproject.toml Adds [dependency-groups].dev (includes build) while retaining pip’s [project.optional-dependencies].dev.
uv.lock New committed lockfile to make dev/CI resolution reproducible.
.gitignore Stops ignoring uv.lock so it can be committed.
.github/workflows/ci.yml Migrates CI to uv and expands to Python 3.11/3.12 matrix with gated pyright/codecov steps.
.github/workflows/publish.yml Migrates release build from python -m build to uv build and installs uv via setup-uv.
CONTRIBUTING.md Updates contributor setup/validation commands to uv equivalents.
CLAUDE.md Updates canonical validation command to uv-based workflow.
docs/cli-ops.md Updates dev install instructions to uv sync and notes committed lock usage.
docs/codecov-ops.md Updates local coverage/validation commands to uv equivalents.
docs/manifest-loader-ops.md Updates fixture regen guidance to reference the dev group + uv sync.
.claude/rules/python-build.md Documents uv-managed dev env + matrix floor/ceiling conventions.
.claude/rules/ci-supply-chain.md Updates CI supply-chain/matrix guidance (but contains a Codecov gating inconsistency).
Comments suppressed due to low confidence (1)

.github/workflows/publish.yml:61

  • Same as above for the PyPI job: setup-uv has no python-version, so uv build may run under a moving default Python from the runner image. Pin the Python version explicitly (e.g., 3.11) for deterministic release artifacts.
      - name: Set up uv
        uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b  # v8.1.0
      - name: Build sdist + wheel
        run: uv build

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .claude/rules/ci-supply-chain.md Outdated
Comment thread .github/workflows/publish.yml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/rules/ci-supply-chain.md:
- Line 78: The rule text currently instructs to "Gate on `matrix.python-version
== '3.13'`" but CI actually gates uploads on 3.12; update that mention to
"matrix.python-version == '3.12'". Locate the string `matrix.python-version ==
'3.13'` in the rule text (in the ci-supply-chain rule) and replace the version
token '3.13' with '3.12' so the documentation matches current CI behavior.

In @.github/workflows/ci.yml:
- Around line 32-33: The checkout step using
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 should be updated to
disable credential persistence by adding persist-credentials: false to the step;
modify the Checkout step (the block that uses actions/checkout...) to include
the key persist-credentials with value false so the GITHUB_TOKEN is not written
into local git config during this read-only CI job.

In @.github/workflows/publish.yml:
- Around line 35-36: The Checkout steps that use actions/checkout (the steps
with uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5) need to
include persist-credentials: false; update both occurrences of the "Checkout"
step in the publish workflow to add the key persist-credentials: false under the
step so the checkout does not persist the GITHUB_TOKEN in git config.

In `@docs/cli-ops.md`:
- Around line 41-45: Clarify that the pip fallback is not fully equivalent to
the uv path: update the paragraph around 'uv sync --dev' and the fallback 'pip
install -e ".[dev]"' to add a short note stating that '[dependency-groups].dev'
(used by uv sync --dev) includes the 'build' tool and possibly other uv-only
deltas, so pip users should not assume identical tooling coverage; reference the
symbols 'uv sync --dev', 'pip install -e ".[dev]"', '[dependency-groups].dev',
and 'build' in the note so readers can see exactly which parts differ.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ef1e077-3d4c-46b1-9fa5-f7b2968b66e2

📥 Commits

Reviewing files that changed from the base of the PR and between ff8e6b7 and 3d7354a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .claude/rules/ci-supply-chain.md
  • .claude/rules/python-build.md
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • .gitignore
  • CLAUDE.md
  • CONTRIBUTING.md
  • README.md
  • docs/cli-ops.md
  • docs/codecov-ops.md
  • docs/manifest-loader-ops.md
  • pyproject.toml
💤 Files with no reviewable changes (1)
  • .gitignore

Comment thread .claude/rules/ci-supply-chain.md Outdated
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/publish.yml
Comment thread docs/cli-ops.md Outdated
Five fixes from PR #95 review (all real issues, no false positives):

- `.claude/rules/ci-supply-chain.md`: codecov-gate example said
  `matrix.python-version == '3.13'` (stale from the 3.13 matrix);
  current CI gates on `'3.12'`. Updated and noted the flip-back when
  issue #96 lands. [coderabbit/copilot — same finding, threads 1+3]
- `.github/workflows/ci.yml`: `actions/checkout` now sets
  `persist-credentials: false` — CI is read-only; least-privilege
  for the GITHUB_TOKEN. [coderabbit — thread 4]
- `.github/workflows/publish.yml`: same `persist-credentials: false`
  on both `Checkout` steps; same justification (build job doesn't
  push to git). [coderabbit — thread 5]
- `.github/workflows/publish.yml`: `astral-sh/setup-uv` now pins
  `python-version: "3.11"` on both jobs for release-build
  reproducibility. Without it, `uv build` picks whatever
  `ubuntu-latest` happens to ship. [copilot — thread 2]
- `docs/cli-ops.md`: clarify that the `pip install -e ".[dev]"`
  fallback omits the `build` package (uv-only delta in
  `[dependency-groups].dev` powering wheel_smoke). Pip users
  shouldn't assume identical tooling coverage. [coderabbit — thread 6]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary

All 6 review threads addressed in commit 81413cb. Five real issues fixed; zero false positives.

Fixed (5 items)

File Line Issue Source Fix
.claude/rules/ci-supply-chain.md 78 Codecov gate example said '3.13' but current CI gates on '3.12' coderabbit + copilot (dup) Updated to '3.12'; noted flip-back when #96 lands
.github/workflows/ci.yml 32-33 actions/checkout didn't set persist-credentials: false coderabbit Added with: persist-credentials: false — CI is read-only
.github/workflows/publish.yml 36, 57 Same persist-credentials hardening missing on both checkout steps coderabbit Same fix applied to both jobs
.github/workflows/publish.yml 38, 59 astral-sh/setup-uv lacked python-version pin (release-build reproducibility) copilot Pinned python-version: "3.11" on both jobs
docs/cli-ops.md 45 pip install -e ".[dev]" fallback line read as equivalent to uv sync --dev, but build is a uv-only delta coderabbit Reformatted to two-line snippet with the omission noted inline

False positives (0)

All review comments were actionable; no false positives to document.

@wjduenow
wjduenow merged commit c802b32 into dev May 19, 2026
3 checks passed
@wjduenow
wjduenow deleted the feature/uv-migration branch May 19, 2026 18:42
wjduenow added a commit that referenced this pull request May 20, 2026
Branch discipline: test releases are now cut from `dev` (was "any
branch") and full releases from `main` (unchanged). The publish.yml
prerelease-flag routing is unchanged — it's the mechanism; the branch
is the maintainer discipline the skill enforces in pre-flight. dev is
the in-development line feeding TestPyPI; main is the released line
feeding PyPI.

Also refreshes the skill for the uv migration (PRs #95/#97):
- validation command → `uv sync --dev && uv run ruff/pyright/pytest`
- `python -m build` → `uv build` (both build blocks + PR-body string)
- allowed-tools gains `Bash(uv *)`; compatibility frontmatter updated
- left the TestPyPI clean-room `pip install` smoke test as-is (it
  deliberately installs the published artifact in a throwaway venv)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wjduenow added a commit that referenced this pull request May 20, 2026
* 93: silence pydantic UserWarning for LLMRequest.schema field shadow (#94)

* 93: silence pydantic UserWarning for LLMRequest.schema field shadow

The `schema` field name on `LLMRequest` is part of the documented audit-log
contract (safety-layer.md DEC-014), but it shadows Pydantic v1's deprecated
`BaseModel.schema` method, triggering a UserWarning at class-creation time
that surfaces on every fresh import / CLI invocation.

Wrap the class definition in `warnings.catch_warnings()` with a targeted
filter — narrowest blast radius; no global filter mutation, the contract
stays unchanged. Adds a subprocess-import regression test under `-W
error::UserWarning` so future shape changes can't silently reintroduce it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: document Pydantic field-name-shadow suppression pattern

Compounding update from PR #94: capture the `catch_warnings()` scope-to-class
convention so the next field-name-shadow case (audit-log contract + Pydantic
v1 BaseModel attribute collision) doesn't get "fixed" by renaming or a global
filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: address PR review feedback

- models.py: relax warning-filter message from exact literal to regex
  (`Field name "schema".*shadows.*`) so future Pydantic message-wording
  drift still gets caught. Drops the experimental `module=` qualifier
  (over-narrowing — filter stopped matching the live warning).
- test_models.py: add `timeout=10` to the subprocess import smoke,
  matching the precedent in `tests/cli/test_subprocess_smoke.py` so a
  pathological import hang fails fast instead of stalling the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: reconcile README + docs with current v0.1 codebase (#90)

Audited the root README and docs/ ops files against the shipped code:

- demo profiles.yml: add maximum_bytes_billed: 1000000000 so the README
  quick start actually works — the demo's materialised-sample CTAS over
  the full ~2.27M-row bikeshare_trips source exceeds the adapter's 100 MB
  default cap. Parity-test docstring updated to document the new rewrite.
- README: add the kept-uncertain tier to the expected-output section
  (four-tier taxonomy was undocumented), complete the generate/lint flag
  lists in the CLI section, mention lint --model.
- docs/audits.md: fix stale audit_schema_version values (safety 1 -> 3,
  prune 1 -> 2) bumped by issues #54/#55.
- docs/e2e-smoke-test.md: fix stale cross-reference claiming the README
  quick start does a manual profile rewrite — it uses signalforge init-demo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12 (#95)

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12/3.13

Mirrors clauditor's build setup. uv becomes the canonical dev install
(`uv sync --dev`); `uv.lock` is committed; CI runs uv-managed steps
across a 3-Python matrix. `pip install -e ".[dev]"` is retained for
back-compat — `[project.optional-dependencies].dev` and the new
`[dependency-groups].dev` are kept in sync.

- `pyproject.toml`: add `[dependency-groups].dev` (PEP 735); only
  delta vs the pip-extra is `build>=1.2,<2` for the wheel_smoke marker.
- `.github/workflows/ci.yml`: replace setup-python + pip install with
  `astral-sh/setup-uv@v8.1.0` (SHA-pinned per ci-supply-chain.md) +
  `uv sync --dev`; wrap lint/test steps with `uv run`. Matrix runs
  ruff + pytest on all three versions; pyright gated to the matrix
  floor (3.11, matching pyright.pythonVersion); codecov upload gated
  to the matrix ceiling (3.13) so coverage doesn't double-upload.
- `.github/workflows/publish.yml`: `python -m build` → `uv build` in
  both publish-testpypi and publish-pypi jobs.
- `.gitignore`: drop `uv.lock` from the ignore list (bark scaffolding
  artifact comment is stale — the canonical lock is committed now).
- `CLAUDE.md` / `CONTRIBUTING.md` / `README.md` / `docs/cli-ops.md` /
  `docs/codecov-ops.md` / `docs/manifest-loader-ops.md`: switch
  copy-pasteable dev commands to `uv sync --dev` + `uv run …`.
- `.claude/rules/python-build.md`: replace "Editable install
  (zsh-safe)" with a uv-managed dev environment section; refresh
  issue-#46 floor-pinning to describe the matrix.
- `.claude/rules/ci-supply-chain.md`: add `astral-sh/setup-uv` to the
  SHA-pinning example list; graduate DEC-003 from single-Python to a
  3-Python matrix with the pyright / codecov gating rationale.

Local validation: `uv run ruff check .` / `uv run ruff format --check
.` / `uv run pyright` / `uv run pytest` all clean under Python 3.13;
`uv run --python 3.11 pytest --no-cov` and `--python 3.12` both
1830/1830 pass. wheel_smoke passes under `uv run`. (Six pre-existing
WSL2-only symlink-loop test failures on 3.13 unaffected; pass in GHA.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: drop 3.13 from matrix; document path-safety follow-up

CI exposed six 3.13-only test failures under tests/_common/,
tests/diff/, tests/grade/, tests/manifest/. Root cause: Python 3.13
changed Path.resolve() to raise OSError(errno.ELOOP) on cyclic
symlinks instead of RuntimeError. SignalForge's _canonicalise_path
and the manifest loader's path canonicalisation catch RuntimeError
only — on 3.13 the cycle now escapes as OSError or short-circuits
to a different typed error before the loop check fires.

Fixing it touches production code (signalforge/_common/path_safety.py
and signalforge/manifest/loader.py) plus four test fixtures, which is
substantial scope creep for a tooling-migration PR. Narrow the matrix
to 3.11 / 3.12 (both green: 1830 passed locally and in CI) and track
the 3.13 work in a follow-up issue.

- ci.yml: matrix python-version: ["3.11", "3.12"]; codecov-upload gate
  flips from 3.13 → 3.12 (matrix ceiling). Comment block records the
  reason and points at the follow-up.
- CLAUDE.md / CONTRIBUTING.md / docs/cli-ops.md / python-build.md /
  ci-supply-chain.md: every "3.11 / 3.12 / 3.13" reference now reads
  "3.11 / 3.12"; ci-supply-chain.md and python-build.md add a "3.13
  is deferred" paragraph naming the catch-site fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: address CodeRabbit + Copilot review feedback

Five fixes from PR #95 review (all real issues, no false positives):

- `.claude/rules/ci-supply-chain.md`: codecov-gate example said
  `matrix.python-version == '3.13'` (stale from the 3.13 matrix);
  current CI gates on `'3.12'`. Updated and noted the flip-back when
  issue #96 lands. [coderabbit/copilot — same finding, threads 1+3]
- `.github/workflows/ci.yml`: `actions/checkout` now sets
  `persist-credentials: false` — CI is read-only; least-privilege
  for the GITHUB_TOKEN. [coderabbit — thread 4]
- `.github/workflows/publish.yml`: same `persist-credentials: false`
  on both `Checkout` steps; same justification (build job doesn't
  push to git). [coderabbit — thread 5]
- `.github/workflows/publish.yml`: `astral-sh/setup-uv` now pins
  `python-version: "3.11"` on both jobs for release-build
  reproducibility. Without it, `uv build` picks whatever
  `ubuntu-latest` happens to ship. [copilot — thread 2]
- `docs/cli-ops.md`: clarify that the `pip install -e ".[dev]"`
  fallback omits the `build` package (uv-only delta in
  `[dependency-groups].dev` powering wheel_smoke). Pip users
  shouldn't assume identical tooling coverage. [coderabbit — thread 6]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: publish MkDocs Material site to GitHub Pages on push to main (#97)

* docs: publish MkDocs Material site to GitHub Pages on push to main

Mirrors clauditor's docs-publishing setup. The published site at
https://wjduenow.github.io/SignalForge/ redeploys on every push to
main; the gh-pages branch is generated, not authored.

- `mkdocs.yml`: Material theme with light/dark toggle, search +
  include-markdown plugins, mermaid via pymdownx.superfences,
  exclude_docs: research/ (internal analysis stays off the site),
  edit_uri: edit/dev/docs/ (doc edits land on dev like every other
  PR; main is the release line). Nav: Home + CLI Reference +
  Pipeline Stages (manifest/warehouse/safety/draft/prune/grade/diff)
  + Audits & Sidecars + e2e + coverage.
- `docs/index.md`: 4-line include-markdown stub pulling in the root
  README. Mirrors clauditor verbatim — README stays the canonical
  authored home doc; site home auto-syncs on every build.
- `.github/workflows/ci.yml`: new `docs:` job gated on
  `github.ref == 'refs/heads/main' && github.event_name == 'push'`
  with job-scoped `permissions: contents: write` (workflow default
  stays `contents: read`). Uses pinned `astral-sh/setup-uv@v8.1.0`
  with `python-version: "3.11"` (matches publish.yml floor); runs
  `uv run mkdocs gh-deploy --force --no-history`. Deliberately does
  NOT set `persist-credentials: false` on the checkout step —
  `gh-deploy` needs the persisted GITHUB_TOKEN to push to gh-pages.
- `pyproject.toml`: add `mkdocs-material>=9.0` and
  `mkdocs-include-markdown-plugin>=6.0` to `[dependency-groups].dev`
  only. Pip contributors don't need to build docs; this stays a
  uv-only delta (alongside `build` for wheel_smoke).
- `.gitignore`: ignore `site/` (mkdocs build output).
- `README.md`: docs badge top-of-file.
- `CLAUDE.md` / `CONTRIBUTING.md`: one-liner pointing at the
  published site and the deploy contract.
- `.claude/rules/docs-publishing.md` (new): full deploy contract —
  trigger rule, include-markdown pattern, edit_uri choice,
  persist-credentials exception, exclude_docs convention, local
  build recipe, when to update mkdocs.yml vs the docs themselves,
  first-time GH Pages setup the maintainer does once.

Local validation: `uv run mkdocs build` succeeds (warnings on
repo-internal links to plans/super/ and .claude/rules/ are expected
and matched in clauditor — those aren't part of the published
site). `uv run ruff check .` / `ruff format --check .` clean.
1823/1823 tests pass on 3.13 (6 pre-existing #96 failures
unrelated).

Post-merge maintainer step (once): Settings → Pages → source =
"Deploy from a branch", branch = gh-pages, folder = / (root). The
first push to main after this merges lands the gh-pages branch;
toggle Pages on right after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address PR #97 review — split docs dep group + PR-time build gate

Two Copilot suggestions, both real wins:

- Extract `[dependency-groups].docs` (mkdocs-material +
  mkdocs-include-markdown-plugin only). Both CI docs jobs now
  `uv run --only-group docs ...` so they pull just MkDocs + plugins,
  not the heavy dev set (dbt-core, pyright, pytest). `dev` includes
  `docs` via `{include-group = "docs"}` so `uv sync --dev` still
  gives contributors everything. [thread: ci.yml deploy pulled --dev]
- Add a read-only `docs-build` job that runs on every PR + push
  (`uv run --only-group docs mkdocs build`, no write perms). Catches
  a broken mkdocs.yml / plugin config / include-markdown syntax error
  at PR time instead of silently merging and only failing the
  post-merge gh-pages deploy. [thread: no pre-merge docs build signal]

The third thread (Home page "Edit this page" link points at the
docs/index.md include stub rather than README.md) is an accepted
limitation of the include-markdown pattern — Material has no per-page
edit-button hide without a template override, and clauditor's
identical setup accepts it. The stub a contributor lands on literally
documents where the real content lives. Documented in the review
summary; no code change.

`.claude/rules/docs-publishing.md` updated: two-job structure, the
docs dependency-group split, and the "new plugin goes in docs not
dev" convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release-manager: cut TestPyPI from dev, PyPI from main + uv refresh

Branch discipline: test releases are now cut from `dev` (was "any
branch") and full releases from `main` (unchanged). The publish.yml
prerelease-flag routing is unchanged — it's the mechanism; the branch
is the maintainer discipline the skill enforces in pre-flight. dev is
the in-development line feeding TestPyPI; main is the released line
feeding PyPI.

Also refreshes the skill for the uv migration (PRs #95/#97):
- validation command → `uv sync --dev && uv run ruff/pyright/pytest`
- `python -m build` → `uv build` (both build blocks + PR-body string)
- allowed-tools gains `Bash(uv *)`; compatibility frontmatter updated
- left the TestPyPI clean-room `pip install` smoke test as-is (it
  deliberately installs the published artifact in a throwaway venv)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump to 0.1.0rc3 for test release

* release-manager: pin clean-room smoke test to Python >=3.11

The Step 5 TestPyPI smoke test used a bare `python -m venv`, which
picks up whatever `python3` is on PATH — on a host where that's 3.10,
pip silently ignores the new release (requires-python >=3.11) and only
the pre-floor 0.1.0rc1 installs, producing a confusing "Could not find
a version that satisfies the requirement" right after a green publish.

Surfaced cutting v0.1.0rc3: the first clean-room attempt used the
system 3.10 and rejected rc3/rc2 as "Requires-Python >=3.11".

Fix: provision the interpreter explicitly via `uv venv --python 3.11`
(the requires-python floor — verifies installability on the minimum
supported version; uv auto-fetches 3.11 if absent) and `uv pip install
--python <venv>`. Also documents how to distinguish this from the
separate TestPyPI Fastly-cache propagation lag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: disable setup-uv cache (enable-cache: false) on all steps

CodeRabbit flagged on PR #98: setup-uv's default persists the uv cache
to the GitHub Actions Cache. On a public repo, pull_request runs share
a cache scope with the base branch, so a fork PR can poison an entry a
later trusted run restores — a cross-trust-boundary cache-poisoning
vector. Set `enable-cache: false` explicitly on every setup-uv step:
ci.yml (lint-test, docs-build, docs-deploy) + publish.yml (testpypi,
pypi). Runs are short enough that the lost cache is negligible; the
publish/deploy steps especially must never restore a poisoned cache
into a published artifact or the gh-pages deploy.

Documents the convention in .claude/rules/ci-supply-chain.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow wjduenow mentioned this pull request May 22, 2026
wjduenow added a commit that referenced this pull request May 22, 2026
* 93: silence pydantic UserWarning for LLMRequest.schema field shadow (#94)

* 93: silence pydantic UserWarning for LLMRequest.schema field shadow

The `schema` field name on `LLMRequest` is part of the documented audit-log
contract (safety-layer.md DEC-014), but it shadows Pydantic v1's deprecated
`BaseModel.schema` method, triggering a UserWarning at class-creation time
that surfaces on every fresh import / CLI invocation.

Wrap the class definition in `warnings.catch_warnings()` with a targeted
filter — narrowest blast radius; no global filter mutation, the contract
stays unchanged. Adds a subprocess-import regression test under `-W
error::UserWarning` so future shape changes can't silently reintroduce it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: document Pydantic field-name-shadow suppression pattern

Compounding update from PR #94: capture the `catch_warnings()` scope-to-class
convention so the next field-name-shadow case (audit-log contract + Pydantic
v1 BaseModel attribute collision) doesn't get "fixed" by renaming or a global
filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: address PR review feedback

- models.py: relax warning-filter message from exact literal to regex
  (`Field name "schema".*shadows.*`) so future Pydantic message-wording
  drift still gets caught. Drops the experimental `module=` qualifier
  (over-narrowing — filter stopped matching the live warning).
- test_models.py: add `timeout=10` to the subprocess import smoke,
  matching the precedent in `tests/cli/test_subprocess_smoke.py` so a
  pathological import hang fails fast instead of stalling the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: reconcile README + docs with current v0.1 codebase (#90)

Audited the root README and docs/ ops files against the shipped code:

- demo profiles.yml: add maximum_bytes_billed: 1000000000 so the README
  quick start actually works — the demo's materialised-sample CTAS over
  the full ~2.27M-row bikeshare_trips source exceeds the adapter's 100 MB
  default cap. Parity-test docstring updated to document the new rewrite.
- README: add the kept-uncertain tier to the expected-output section
  (four-tier taxonomy was undocumented), complete the generate/lint flag
  lists in the CLI section, mention lint --model.
- docs/audits.md: fix stale audit_schema_version values (safety 1 -> 3,
  prune 1 -> 2) bumped by issues #54/#55.
- docs/e2e-smoke-test.md: fix stale cross-reference claiming the README
  quick start does a manual profile rewrite — it uses signalforge init-demo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12 (#95)

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12/3.13

Mirrors clauditor's build setup. uv becomes the canonical dev install
(`uv sync --dev`); `uv.lock` is committed; CI runs uv-managed steps
across a 3-Python matrix. `pip install -e ".[dev]"` is retained for
back-compat — `[project.optional-dependencies].dev` and the new
`[dependency-groups].dev` are kept in sync.

- `pyproject.toml`: add `[dependency-groups].dev` (PEP 735); only
  delta vs the pip-extra is `build>=1.2,<2` for the wheel_smoke marker.
- `.github/workflows/ci.yml`: replace setup-python + pip install with
  `astral-sh/setup-uv@v8.1.0` (SHA-pinned per ci-supply-chain.md) +
  `uv sync --dev`; wrap lint/test steps with `uv run`. Matrix runs
  ruff + pytest on all three versions; pyright gated to the matrix
  floor (3.11, matching pyright.pythonVersion); codecov upload gated
  to the matrix ceiling (3.13) so coverage doesn't double-upload.
- `.github/workflows/publish.yml`: `python -m build` → `uv build` in
  both publish-testpypi and publish-pypi jobs.
- `.gitignore`: drop `uv.lock` from the ignore list (bark scaffolding
  artifact comment is stale — the canonical lock is committed now).
- `CLAUDE.md` / `CONTRIBUTING.md` / `README.md` / `docs/cli-ops.md` /
  `docs/codecov-ops.md` / `docs/manifest-loader-ops.md`: switch
  copy-pasteable dev commands to `uv sync --dev` + `uv run …`.
- `.claude/rules/python-build.md`: replace "Editable install
  (zsh-safe)" with a uv-managed dev environment section; refresh
  issue-#46 floor-pinning to describe the matrix.
- `.claude/rules/ci-supply-chain.md`: add `astral-sh/setup-uv` to the
  SHA-pinning example list; graduate DEC-003 from single-Python to a
  3-Python matrix with the pyright / codecov gating rationale.

Local validation: `uv run ruff check .` / `uv run ruff format --check
.` / `uv run pyright` / `uv run pytest` all clean under Python 3.13;
`uv run --python 3.11 pytest --no-cov` and `--python 3.12` both
1830/1830 pass. wheel_smoke passes under `uv run`. (Six pre-existing
WSL2-only symlink-loop test failures on 3.13 unaffected; pass in GHA.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: drop 3.13 from matrix; document path-safety follow-up

CI exposed six 3.13-only test failures under tests/_common/,
tests/diff/, tests/grade/, tests/manifest/. Root cause: Python 3.13
changed Path.resolve() to raise OSError(errno.ELOOP) on cyclic
symlinks instead of RuntimeError. SignalForge's _canonicalise_path
and the manifest loader's path canonicalisation catch RuntimeError
only — on 3.13 the cycle now escapes as OSError or short-circuits
to a different typed error before the loop check fires.

Fixing it touches production code (signalforge/_common/path_safety.py
and signalforge/manifest/loader.py) plus four test fixtures, which is
substantial scope creep for a tooling-migration PR. Narrow the matrix
to 3.11 / 3.12 (both green: 1830 passed locally and in CI) and track
the 3.13 work in a follow-up issue.

- ci.yml: matrix python-version: ["3.11", "3.12"]; codecov-upload gate
  flips from 3.13 → 3.12 (matrix ceiling). Comment block records the
  reason and points at the follow-up.
- CLAUDE.md / CONTRIBUTING.md / docs/cli-ops.md / python-build.md /
  ci-supply-chain.md: every "3.11 / 3.12 / 3.13" reference now reads
  "3.11 / 3.12"; ci-supply-chain.md and python-build.md add a "3.13
  is deferred" paragraph naming the catch-site fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: address CodeRabbit + Copilot review feedback

Five fixes from PR #95 review (all real issues, no false positives):

- `.claude/rules/ci-supply-chain.md`: codecov-gate example said
  `matrix.python-version == '3.13'` (stale from the 3.13 matrix);
  current CI gates on `'3.12'`. Updated and noted the flip-back when
  issue #96 lands. [coderabbit/copilot — same finding, threads 1+3]
- `.github/workflows/ci.yml`: `actions/checkout` now sets
  `persist-credentials: false` — CI is read-only; least-privilege
  for the GITHUB_TOKEN. [coderabbit — thread 4]
- `.github/workflows/publish.yml`: same `persist-credentials: false`
  on both `Checkout` steps; same justification (build job doesn't
  push to git). [coderabbit — thread 5]
- `.github/workflows/publish.yml`: `astral-sh/setup-uv` now pins
  `python-version: "3.11"` on both jobs for release-build
  reproducibility. Without it, `uv build` picks whatever
  `ubuntu-latest` happens to ship. [copilot — thread 2]
- `docs/cli-ops.md`: clarify that the `pip install -e ".[dev]"`
  fallback omits the `build` package (uv-only delta in
  `[dependency-groups].dev` powering wheel_smoke). Pip users
  shouldn't assume identical tooling coverage. [coderabbit — thread 6]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: publish MkDocs Material site to GitHub Pages on push to main (#97)

* docs: publish MkDocs Material site to GitHub Pages on push to main

Mirrors clauditor's docs-publishing setup. The published site at
https://wjduenow.github.io/SignalForge/ redeploys on every push to
main; the gh-pages branch is generated, not authored.

- `mkdocs.yml`: Material theme with light/dark toggle, search +
  include-markdown plugins, mermaid via pymdownx.superfences,
  exclude_docs: research/ (internal analysis stays off the site),
  edit_uri: edit/dev/docs/ (doc edits land on dev like every other
  PR; main is the release line). Nav: Home + CLI Reference +
  Pipeline Stages (manifest/warehouse/safety/draft/prune/grade/diff)
  + Audits & Sidecars + e2e + coverage.
- `docs/index.md`: 4-line include-markdown stub pulling in the root
  README. Mirrors clauditor verbatim — README stays the canonical
  authored home doc; site home auto-syncs on every build.
- `.github/workflows/ci.yml`: new `docs:` job gated on
  `github.ref == 'refs/heads/main' && github.event_name == 'push'`
  with job-scoped `permissions: contents: write` (workflow default
  stays `contents: read`). Uses pinned `astral-sh/setup-uv@v8.1.0`
  with `python-version: "3.11"` (matches publish.yml floor); runs
  `uv run mkdocs gh-deploy --force --no-history`. Deliberately does
  NOT set `persist-credentials: false` on the checkout step —
  `gh-deploy` needs the persisted GITHUB_TOKEN to push to gh-pages.
- `pyproject.toml`: add `mkdocs-material>=9.0` and
  `mkdocs-include-markdown-plugin>=6.0` to `[dependency-groups].dev`
  only. Pip contributors don't need to build docs; this stays a
  uv-only delta (alongside `build` for wheel_smoke).
- `.gitignore`: ignore `site/` (mkdocs build output).
- `README.md`: docs badge top-of-file.
- `CLAUDE.md` / `CONTRIBUTING.md`: one-liner pointing at the
  published site and the deploy contract.
- `.claude/rules/docs-publishing.md` (new): full deploy contract —
  trigger rule, include-markdown pattern, edit_uri choice,
  persist-credentials exception, exclude_docs convention, local
  build recipe, when to update mkdocs.yml vs the docs themselves,
  first-time GH Pages setup the maintainer does once.

Local validation: `uv run mkdocs build` succeeds (warnings on
repo-internal links to plans/super/ and .claude/rules/ are expected
and matched in clauditor — those aren't part of the published
site). `uv run ruff check .` / `ruff format --check .` clean.
1823/1823 tests pass on 3.13 (6 pre-existing #96 failures
unrelated).

Post-merge maintainer step (once): Settings → Pages → source =
"Deploy from a branch", branch = gh-pages, folder = / (root). The
first push to main after this merges lands the gh-pages branch;
toggle Pages on right after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address PR #97 review — split docs dep group + PR-time build gate

Two Copilot suggestions, both real wins:

- Extract `[dependency-groups].docs` (mkdocs-material +
  mkdocs-include-markdown-plugin only). Both CI docs jobs now
  `uv run --only-group docs ...` so they pull just MkDocs + plugins,
  not the heavy dev set (dbt-core, pyright, pytest). `dev` includes
  `docs` via `{include-group = "docs"}` so `uv sync --dev` still
  gives contributors everything. [thread: ci.yml deploy pulled --dev]
- Add a read-only `docs-build` job that runs on every PR + push
  (`uv run --only-group docs mkdocs build`, no write perms). Catches
  a broken mkdocs.yml / plugin config / include-markdown syntax error
  at PR time instead of silently merging and only failing the
  post-merge gh-pages deploy. [thread: no pre-merge docs build signal]

The third thread (Home page "Edit this page" link points at the
docs/index.md include stub rather than README.md) is an accepted
limitation of the include-markdown pattern — Material has no per-page
edit-button hide without a template override, and clauditor's
identical setup accepts it. The stub a contributor lands on literally
documents where the real content lives. Documented in the review
summary; no code change.

`.claude/rules/docs-publishing.md` updated: two-job structure, the
docs dependency-group split, and the "new plugin goes in docs not
dev" convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release-manager: cut TestPyPI from dev, PyPI from main + uv refresh

Branch discipline: test releases are now cut from `dev` (was "any
branch") and full releases from `main` (unchanged). The publish.yml
prerelease-flag routing is unchanged — it's the mechanism; the branch
is the maintainer discipline the skill enforces in pre-flight. dev is
the in-development line feeding TestPyPI; main is the released line
feeding PyPI.

Also refreshes the skill for the uv migration (PRs #95/#97):
- validation command → `uv sync --dev && uv run ruff/pyright/pytest`
- `python -m build` → `uv build` (both build blocks + PR-body string)
- allowed-tools gains `Bash(uv *)`; compatibility frontmatter updated
- left the TestPyPI clean-room `pip install` smoke test as-is (it
  deliberately installs the published artifact in a throwaway venv)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump to 0.1.0rc3 for test release

* release-manager: pin clean-room smoke test to Python >=3.11

The Step 5 TestPyPI smoke test used a bare `python -m venv`, which
picks up whatever `python3` is on PATH — on a host where that's 3.10,
pip silently ignores the new release (requires-python >=3.11) and only
the pre-floor 0.1.0rc1 installs, producing a confusing "Could not find
a version that satisfies the requirement" right after a green publish.

Surfaced cutting v0.1.0rc3: the first clean-room attempt used the
system 3.10 and rejected rc3/rc2 as "Requires-Python >=3.11".

Fix: provision the interpreter explicitly via `uv venv --python 3.11`
(the requires-python floor — verifies installability on the minimum
supported version; uv auto-fetches 3.11 if absent) and `uv pip install
--python <venv>`. Also documents how to distinguish this from the
separate TestPyPI Fastly-cache propagation lag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: disable setup-uv cache (enable-cache: false) on all steps

CodeRabbit flagged on PR #98: setup-uv's default persists the uv cache
to the GitHub Actions Cache. On a public repo, pull_request runs share
a cache scope with the base branch, so a fork PR can poison an entry a
later trusted run restores — a cross-trust-boundary cache-poisoning
vector. Set `enable-cache: false` explicitly on every setup-uv step:
ci.yml (lint-test, docs-build, docs-deploy) + publish.yml (testpypi,
pypi). Runs are short enough that the lost cache is negligible; the
publish/deploy steps especially must never restore a poisoned cache
into a published artifact or the gh-pages deploy.

Documents the convention in .claude/rules/ci-supply-chain.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: begin 0.2.0.dev0 (#100)

* docs: README install instructions for the live PyPI release

signalforge-dbt 0.1.0 is on PyPI, so lead the install with
`pip install signalforge-dbt` instead of the clone+uv-sync path.

- Status line: "Not yet on PyPI" → "Live on PyPI — pip install signalforge-dbt".
- Quick start §1: pip install first + `signalforge --version` verify; note
  the distribution-vs-import name; add `uv tool install` / `pipx` as the
  isolated-CLI alternative; demote the clone + `uv sync --dev` path to a
  contributing pointer at CONTRIBUTING.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #104: ingest external dbt schema.yml tests into CandidateSchema (#106)

* Add super plan for #104: ingest external dbt schema.yml tests

Library-only seam (signalforge.ingest.read_schema -> IngestResult).
prune-existing CLI subcommand split to fast-follow #105.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mark #104 plan published (PR #106)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Devolve #104 plan to beads (epic bd_1-scaffolding-ky3, 8 tasks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.1: scaffold signalforge.ingest package + IngestError hierarchy + exit-code lockstep

US-001 of issue #104. Adds the signalforge.ingest subpackage scaffold and
its six-class typed-error hierarchy (DEC-001), wired into the CLI four-tier
exit-code taxonomy and the 7th AST scan.

- src/signalforge/ingest/__init__.py: package init re-exporting the
  IngestError surface.
- src/signalforge/ingest/errors.py: IngestError base (remediation +
  ↳ Remediation rendering, repr-safe values) plus five concretes —
  IngestSchemaNotFoundError / IngestSchemaParseError /
  IngestSchemaTooLargeError (tier 1) and IngestModelNotFoundError /
  IngestAnchorContractError (tier 2).
- cli/_helpers.py: every concrete mapped in _EXCEPTION_TO_EXIT_CODE at its
  tier; IngestError registered as fallback tier-1 (dual-registration).
- tests/test_audit_completeness.py: IngestError added to
  _EXCEPTION_MAPPING_EXCLUDED_BASES; Scan-7 module count bumped 10 -> 11.
- tests/cli/test_exit_codes.py: synthetic-construction branches for the
  (size, limit) and (violations,) constructor shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.2: typed ingest result models (IngestResult, SkippedTest, SkipReason)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.4: anchor-contract validator (fail-loud, collect-all)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.3: dbt test-entry parser (pure mapping)

Pure parse_test_entry(entry, *, column) mapping a single dbt schema.yml
test entry to a CandidateTest or a structured SkippedTest. Handles bare
strings, single-key dicts with inline and arguments:-nested args
(DEC-006), config-key tolerance, malformed-supported skips, custom /
namespaced skips, and best-effort ref()/source() unwrap in
relationships.to (DEC-009). No logging, no I/O, not part of the public
surface. 19-test matrix added (TDD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.5: read_schema orchestrator

Implement the public ingest entry point read_schema(schema, model, *,
project_dir=None) -> IngestResult tying together the parser, anchor
validator, and result models. Steps: str-vs-Path input contract
(Path = file, symlink-hardened via _common.path_safety; str = raw YAML),
size-cap before yaml.safe_load (DEC-005), model-block selection by name
(DEC-006), union+dedupe tests/data_tests (DEC-008), description="" default
(DEC-010), whole-file anchor check. No logging (stage-0 reader). Re-export
read_schema from the package __init__ as the public surface.

Adds the dbt-codegen-shaped fixture and 13 tests: happy path (supported +
skipped tests, dedupe, ref() unwrap, both test keys), str-content input,
and each error path (model-not-found, malformed YAML, oversize, anchor
violation, missing path), plus the disabled-prune acceptance bonus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.6: docs/ingest-ops.md operational reference + mkdocs nav

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.7: Quality gate — fix bugs from code review

- parser: model-level supported test (column=None) now routes to a
  structured malformed skip instead of raising a Pydantic ValidationError
  out of read_schema.
- reader: accepted_values dedupe key now sorts values so reordered value
  sets collapse (DEC-008 "sorted-args" contract).
- parser: clarify _extract_args docstring for non-dict `arguments`.
- tests: regression coverage for both fixes (+3 ingest tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.8: Patterns & Memory — ingest-layer rule + CLAUDE.md + cli-layer count

- New .claude/rules/ingest-layer.md distilling the #104 conventions
  (str-vs-Path contract, fail-loud anchor vs skip-record taxonomy,
  no-logging stage-0, safe-YAML size cap, dbt syntax tolerance,
  11th errors.py exit-code lockstep, deferred #105 CLI).
- CLAUDE.md: #104 shipped-issue entry + public-API surface entry.
- cli-layer.md: scan-7 count 10->11; IngestError joins DemoError as a
  span-tier base (frozenset-only, no fallback) — and aligned _helpers.py
  by removing the IngestError tier-1 fallback to match that precedent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #104: Address PR review feedback (Copilot)

- errors.py: correct IngestError docstring — base is frozenset-only (no
  fallback tier), concretes span tiers 1 and 2 like DemoError.
- __init__.py: refresh module docstring — the full library seam (reader,
  parser, anchor, models) ships in #104, not "typed-error surface only".
- parser.py: _extract_args now drops the structural `arguments` key from
  inline args so a non-mapping `arguments:` can't leak (+ regression test).
- docs/ingest-ops.md: fix usage snippet to use WarehouseAdapter.from_profile
  (the public factory; there is no top-level from_profile) and pass an
  un-entered adapter to prune_tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #105: signalforge prune-existing CLI subcommand (#107)

* Add super plan for #105: signalforge prune-existing CLI subcommand

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mark plan phase published (PR #107)

* Mark plan devolved; record beads manifest (epic bd_1-scaffolding-ad1)

* bd_1-scaffolding-ad1.1: Hoist bare-name model resolver to _helpers

#105 US-001 / DEC-008. Move the bare-name model resolver from lint.py to
signalforge.cli._helpers as _resolve_model_by_key(manifest, key) -> Model so
the future prune-existing subcommand shares one resolver rather than
copy-pasting the body. lint.py keeps a thin _resolve_model_for_lint wrapper
that delegates. Identical behaviour: unique_id/file-path via
Manifest.get_model; bare-name via iter_models with multi-match
disambiguation + disabled-model exclusion + the same ModelNotFoundError
messages. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.2: Add prune-existing add_parser + stub handler

US-002 of #105 — contract surface (DEC-001, DEC-002, DEC-003). Adds
src/signalforge/cli/prune_existing.py with add_parser registering the
prune-existing subcommand and full flag set (positional <model>,
required --schema, --project-dir/--manifest/--profiles-dir,
--scope/--sample-strategy/--format choice flags, --dry-run, and the
--quiet/--verbose/--no-color observability triad). Drops --mode/--write/
--min-score/--select/--estimate per DEC-002/DEC-003. cmd_prune_existing
is a stub returning 0 (full ingest->prune->diff body lands in US-003).

Registered in signalforge.cli.__init__._build_parser. Help strings are
surface 1 of the 5-surface parity contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.5: Add prune-existing --help subprocess smoke

Extends the @pytest.mark.cli_subprocess gate to the prune-existing
subcommand (#105 US-005), mirroring the existing generate/lint/version/
init-demo --help smokes. Asserts returncode 0, the unique --schema flag
in stdout, and the no-traceback floor on stderr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.3: Implement cmd_prune_existing orchestrator (ingest -> prune -> diff)

US-003 of #105. Replaces the US-002 stub with the real ingest -> prune ->
diff orchestrator (no draft, no grade, no LLM call), inside one
try/except Exception boundary (DEC-016 — no traceback ever leaks).

- Resolve project_dir (mirror generate: --project-dir absolute assertion,
  walk-up default); --no-color/--profiles-dir env mutation (DEC-023).
- Resolve <model> via _resolve_model_by_key (bare-name / unique_id /
  file-path). Canonicalise --schema via canonicalise_user_path (DEC-005).
- read_schema(Path, model, project_dir=...) so ingest typed errors fire
  and route via the pre-registered exit-code table (DEC-006 — no bespoke
  CliPruneExisting* wrappers).
- Skipped-test report to stderr: grouped summary + --verbose per-item
  detail, suppressed by --quiet (DEC-007). print_stderr, not _LOGGER.
- PruneConfig --scope/--sample-strategy overrides via model_validate
  (DEC-002). _make_warehouse_adapter test seam (DEC-009); prune_tests owns
  the with-adapter block (un-entered adapter).
- Feed the external schema.yml text as render_diff existing_schema with
  grading_report=None -> kept/kept-uncertain/dropped, never flagged
  (DEC-004); --dry-run -> write_sidecar=False. render_to_text -> stdout.
- 3-stage progress (1/3 ingest, 2/3 prune, 3/3 diff) via the existing
  emit_progress_* helpers, generalised with a total= kwarg (default 5).

Adds an Austin-aligned schema.yml fixture whose four supported test types
reference real stg_bikeshare_trips columns plus two unsupported tests for
the skip path. Tests cover happy path, skipped report (+ --verbose +
--quiet), each error-path exit-code tier with a no-traceback floor,
--dry-run no-disk, --format json, and bare-name vs unique_id resolution,
via a FakeBigQueryClient-backed BigQueryAdapter on the patch seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.4: Add cli-ops prune-existing section + 5-surface parity test

US-004 of #105. Documents the `signalforge prune-existing` subcommand in
docs/cli-ops.md (synopsis, flag-reference table, four-tier exit codes,
skipped-test summary stderr shape, read-only/no-`--write` note, the
unified-diff-against-your-file framing + cosmetic-reformatting caveat,
the no-grading -> never-flagged note, and a worked example). Bumps the
subcommand count from four to six.

Adds tests/cli/test_5_surface_parity_prune_existing.py mirroring
test_5_surface_parity_select.py: asserts the flag set (--schema, --scope,
--sample-strategy, --dry-run, prune-existing) and the read-only intent
appear consistently across argparse help, prune_existing.py help/docstrings,
docs/cli-ops.md, and the plan's DEC list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.6: Quality gate — fix bugs from code review

- prune engine: short-circuit empty candidate before warehouse contact
  (all-skipped schema.yml no longer materialises a temp table for zero
  tests); regression test on the default materialised+sample path.
- prune-existing: scrub \n/\r/\t in the --verbose skipped-test report so
  an operator YAML value can't inject a fake bullet line (mirrors
  format_batch_summary's control-char scrub).
- test: assert the exact kept/dropped mix (7/3) in the --format json test
  so a prune keep-everything/drop-everything regression fails loudly
  (testing-signal.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.6: Quality gate — fix CodeRabbit finding (plan ref)

Correct the prune-existing subprocess-smoke docstring to cite
plans/super/105-prune-existing-cli.md (was 104).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.7: Patterns & Memory — record #105 conventions

- ingest-layer.md: prune-existing shipped (was deferred); record the
  no-bespoke-wrappers (DEC-006), --mode-inert (DEC-002), read-only
  (DEC-003/004) findings.
- cli-layer.md: bare-name resolver hoist done in #105; new section on
  "library errors already mapped -> no per-class wrappers" + no-LLM-stage
  flag-audit + read-only conventions.
- prune-engine.md: empty-candidate short-circuit (no warehouse contact).
- CLAUDE.md: add #105 to shipped issues + CLI surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #105: Address PR review feedback (Copilot + CodeRabbit)

- prune_existing.py: refresh stale module docstring (no longer a "stub";
  the orchestrator is implemented) and note the no-bespoke-wrappers seam.
- prune/engine.py: call _maybe_emit_kept_rate_warning at the new
  empty-candidate return site (no-op via total==0 early-return) to honour
  the "every return site" contract (#51).
- docs/cli-ops.md: subcommand count six -> five; avoid a line starting
  with "#104" (heading parse); trim leading spaces inside the Remediation
  inline code span.
- plans/super/105-prune-existing-cli.md: markdownlint — language on the
  fenced block, avoid "#104" line start.

--profiles-dir non-canonicalisation is intentional (mirrors generate;
dbt places profiles.yml outside the project) — documented as a false
positive on the thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: surface ingest + prune-existing (#104/#105) in user docs (#108)

The external-test ingest layer (#104) and the prune-existing CLI (#105)
only appeared in their own ops pages; the README — the published home
page and product pitch — never mentioned them, and ingest-ops.md still
said the CLI didn't exist.

README:
- "Why this exists" / "What it does": SignalForge now prunes ANY
  generator's tests, not just its own drafts.
- "How it works": add the second (no-LLM) entry path diagram.
- New "Prune the tests you already have" cookbook with a runnable
  example + an availability callout (prune-existing is the v0.2 dev
  line, not yet on the v0.1 PyPI release; install-from-source command).
- CLI list: four -> five subcommands; prune-existing flags; v0.2 marker.
- Roadmap: note prune-existing on the v0.2 row.

Docs:
- ingest-ops.md: fix stale "no prune-existing CLI yet" wording (it
  shipped in #105); cross-link the CLI reference.
- cli-ops.md: cross-link the supported/skipped test taxonomy in
  ingest-ops.md.

Verified: mkdocs build clean; all in-page anchors resolve.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: Python 3.13 compatibility for the path-safety layer (#109)

* #96: Python 3.13 compatibility for the path-safety layer

Python 3.13 (gh-108958) changed `Path.resolve()` to raise
`OSError(errno.ELOOP)` instead of `RuntimeError` on cyclic symlinks, and
made `strict=False` resolution stop raising on a cycle entirely (it returns
a best-effort unresolved path). The three loop-guard sites caught only
`RuntimeError`, so on 3.13 a symlink cycle either escaped as a raw OSError
or slipped silently past the containment check.

Fix every loop-guard site to resolve the input path `strict=True` first
(falling back to `strict=False` only for a genuinely missing target) and to
catch both `RuntimeError` (<= 3.12) and `OSError(errno.ELOOP)` (>= 3.13):

- `signalforge/_common/path_safety.py`
- `signalforge/manifest/loader.py::_canonicalise_path`
- `signalforge/demo/__init__.py::copy_demo`

The demo test's symlink-cycle case no longer skips on Linux/WSL2 and now
asserts the cause is `RuntimeError | OSError`.

Re-add 3.13 to the CI matrix as the ceiling; flip the codecov upload gate
3.12 -> 3.13. Update the deferral prose in CLAUDE.md, CONTRIBUTING.md,
python-build.md, and ci-supply-chain.md.

Validated on 3.11 / 3.12 / 3.13: 1923 passed; ruff + format + pyright clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: keep the manifest non-dict-root guard live (pyright unreachable fix)

`raw: dict[str, Any] = json.load(fh)` asserted a type json.load doesn't
guarantee, so pyright treated the `isinstance(loaded, dict)` root-shape
guard as dead code. Load as `Any`, narrow to `dict[str, Any]` after the
check so the guard stays live. Add a test (`non_dict_root.json` — valid
JSON array root) that exercises the now-reachable branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: narrow strict-resolve fallback to missing-target only (PR review)

Copilot review: the `except OSError` fallback to `resolve(strict=False)`
was too broad — it downgraded ANY non-ELOOP OSError (e.g. PermissionError)
to best-effort resolution, contradicting the "fallback only for a missing
target" intent and potentially weakening the containment check.

Narrow the fallback to `FileNotFoundError` / `NotADirectoryError`; re-raise
every other OSError. Applied to all three loop-guard sites
(_common/path_safety, manifest/loader, demo). Tighten the demo docstring to
state the strict-then-fallback behaviour explicitly. Add a regression test
asserting a PermissionError on the input path propagates rather than being
swallowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: close codecov patch-coverage gaps on the path-safety fix

The codecov upload runs from the 3.13 leg, so the `except RuntimeError`
arms (which only execute on Python <= 3.12) and the new non-ELOOP re-raise
arms showed as uncovered. Address honestly:

- Add regression tests for the non-ELOOP re-raise arms (PermissionError on
  the input path AND project_dir for path_safety; input path for loader;
  dest for demo) — these encode the Copilot fix and are reachable on every
  interpreter.
- `# pragma: no cover` the `except RuntimeError` arms (<=3.12-only cycle
  signal; exercised on the 3.11/3.12 CI legs, structurally unreachable on
  the 3.13 upload leg) and loader's project_dir handler block (defensive —
  `load()` passes an already-resolved project_dir, mirroring the tested
  _common.path_safety helper).

Patch coverage on the touched files is now 100% on the 3.13 upload leg.
Validated on 3.11 / 3.12 / 3.13: 1928 passed; ruff + format + pyright clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: steer pre-release gated-marker runs to the matrix ceiling (3.13) (#110)

The gated markers (bigquery / anthropic / cli_subprocess / e2e / wheel_smoke)
carry no Python-version pin and run on whatever interpreter `uv run` resolves;
no CI job runs them. Following #96 (3.13 is now the matrix ceiling), document
that maintainers should run the packaging/entry-point markers (wheel_smoke,
cli_subprocess) under `uv run --python 3.13`, while the interpreter-invariant
live-service markers stay single-version to avoid multiplying paid API calls.

CONTRIBUTING.md § Pre-release coverage audit + testing-signal.md § Known gap.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: release 0.2.0

* test: close 0.2.0 patch-coverage gaps in ingest + prune-existing (#112)

* test: close 0.2.0 patch-coverage gaps in ingest + prune-existing

Brings ingest/parser.py and ingest/reader.py to 100% and
cli/prune_existing.py to 96% on the default (non-gated) test run,
raising the 0.2.0 release PR's codecov patch coverage from 92.44%.

- parser: ref()/source() non-quoted + arity edges, non-dict bodies,
  multi-key + non-str/dict entries, model-level relationships skip
- reader: path-containment failure, unreadable file (POSIX, non-root),
  non-dict / nameless column entries ignored
- prune-existing: walk-up project-dir resolution (success + failure),
  --no-color env mutation, empty skipped-report guard

Residual: prune_existing.py:270 (real WarehouseAdapter.from_profile,
gated-only) and :447-454 (--profiles-dir resolve error, flaky to
trigger) are documented coverage-policy exclusions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: assert diff.json sidecar durability in prune-existing success tests

Addresses CodeRabbit review on #112: the three new success-path tests
now also assert the default-on .signalforge/diff.json sidecar is
written (DEC-016 e2e convention — exit 0 + no traceback + durable
sidecar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit findings on ingest + prune-existing (#104/#105) (#113)

#3 (ingest reader): enforce the DEC-005 size cap from stat().st_size
BEFORE reading file bytes for a Path input, so an oversize schema.yml is
rejected without first being slurped into memory. The post-resolve
length check stays as the str-path enforcement + backstop.

#2 (prune-existing --dry-run docs): correct cli-ops.md — --dry-run
suppresses the .signalforge/diff.json sidecar, but the fail-closed
.signalforge/prune.jsonl audit is STILL written (every prune run leaves
a durable receipt — the cross-stage invariant; mirrors generate).
Pinned by strengthening test_dry_run_writes_no_sidecar to assert
prune.jsonl is present.

(#1, env-var restore, is left as-is — DEC-023 'mutate, don't restore'
is the deliberate, consistent pattern across all subcommands.)

reader.py back to 100% coverage; full suite 1945 passed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wjduenow added a commit that referenced this pull request May 22, 2026
* docs: README install instructions for the live PyPI release (#102)

signalforge-dbt 0.1.0 is on PyPI, so lead the install with
`pip install signalforge-dbt` instead of the clone+uv-sync path.

- Status line: "Not yet on PyPI" → "Live on PyPI — pip install signalforge-dbt".
- Quick start §1: pip install first + `signalforge --version` verify; note
  the distribution-vs-import name; add `uv tool install` / `pipx` as the
  isolated-CLI alternative; demote the clone + `uv sync --dev` path to a
  contributing pointer at CONTRIBUTING.md.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: release 0.2.0 (#111)

* 93: silence pydantic UserWarning for LLMRequest.schema field shadow (#94)

* 93: silence pydantic UserWarning for LLMRequest.schema field shadow

The `schema` field name on `LLMRequest` is part of the documented audit-log
contract (safety-layer.md DEC-014), but it shadows Pydantic v1's deprecated
`BaseModel.schema` method, triggering a UserWarning at class-creation time
that surfaces on every fresh import / CLI invocation.

Wrap the class definition in `warnings.catch_warnings()` with a targeted
filter — narrowest blast radius; no global filter mutation, the contract
stays unchanged. Adds a subprocess-import regression test under `-W
error::UserWarning` so future shape changes can't silently reintroduce it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: document Pydantic field-name-shadow suppression pattern

Compounding update from PR #94: capture the `catch_warnings()` scope-to-class
convention so the next field-name-shadow case (audit-log contract + Pydantic
v1 BaseModel attribute collision) doesn't get "fixed" by renaming or a global
filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 93: address PR review feedback

- models.py: relax warning-filter message from exact literal to regex
  (`Field name "schema".*shadows.*`) so future Pydantic message-wording
  drift still gets caught. Drops the experimental `module=` qualifier
  (over-narrowing — filter stopped matching the live warning).
- test_models.py: add `timeout=10` to the subprocess import smoke,
  matching the precedent in `tests/cli/test_subprocess_smoke.py` so a
  pathological import hang fails fast instead of stalling the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: reconcile README + docs with current v0.1 codebase (#90)

Audited the root README and docs/ ops files against the shipped code:

- demo profiles.yml: add maximum_bytes_billed: 1000000000 so the README
  quick start actually works — the demo's materialised-sample CTAS over
  the full ~2.27M-row bikeshare_trips source exceeds the adapter's 100 MB
  default cap. Parity-test docstring updated to document the new rewrite.
- README: add the kept-uncertain tier to the expected-output section
  (four-tier taxonomy was undocumented), complete the generate/lint flag
  lists in the CLI section, mention lint --model.
- docs/audits.md: fix stale audit_schema_version values (safety 1 -> 3,
  prune 1 -> 2) bumped by issues #54/#55.
- docs/e2e-smoke-test.md: fix stale cross-reference claiming the README
  quick start does a manual profile rewrite — it uses signalforge init-demo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12 (#95)

* build: migrate dev tooling to uv + widen Python matrix to 3.11/3.12/3.13

Mirrors clauditor's build setup. uv becomes the canonical dev install
(`uv sync --dev`); `uv.lock` is committed; CI runs uv-managed steps
across a 3-Python matrix. `pip install -e ".[dev]"` is retained for
back-compat — `[project.optional-dependencies].dev` and the new
`[dependency-groups].dev` are kept in sync.

- `pyproject.toml`: add `[dependency-groups].dev` (PEP 735); only
  delta vs the pip-extra is `build>=1.2,<2` for the wheel_smoke marker.
- `.github/workflows/ci.yml`: replace setup-python + pip install with
  `astral-sh/setup-uv@v8.1.0` (SHA-pinned per ci-supply-chain.md) +
  `uv sync --dev`; wrap lint/test steps with `uv run`. Matrix runs
  ruff + pytest on all three versions; pyright gated to the matrix
  floor (3.11, matching pyright.pythonVersion); codecov upload gated
  to the matrix ceiling (3.13) so coverage doesn't double-upload.
- `.github/workflows/publish.yml`: `python -m build` → `uv build` in
  both publish-testpypi and publish-pypi jobs.
- `.gitignore`: drop `uv.lock` from the ignore list (bark scaffolding
  artifact comment is stale — the canonical lock is committed now).
- `CLAUDE.md` / `CONTRIBUTING.md` / `README.md` / `docs/cli-ops.md` /
  `docs/codecov-ops.md` / `docs/manifest-loader-ops.md`: switch
  copy-pasteable dev commands to `uv sync --dev` + `uv run …`.
- `.claude/rules/python-build.md`: replace "Editable install
  (zsh-safe)" with a uv-managed dev environment section; refresh
  issue-#46 floor-pinning to describe the matrix.
- `.claude/rules/ci-supply-chain.md`: add `astral-sh/setup-uv` to the
  SHA-pinning example list; graduate DEC-003 from single-Python to a
  3-Python matrix with the pyright / codecov gating rationale.

Local validation: `uv run ruff check .` / `uv run ruff format --check
.` / `uv run pyright` / `uv run pytest` all clean under Python 3.13;
`uv run --python 3.11 pytest --no-cov` and `--python 3.12` both
1830/1830 pass. wheel_smoke passes under `uv run`. (Six pre-existing
WSL2-only symlink-loop test failures on 3.13 unaffected; pass in GHA.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: drop 3.13 from matrix; document path-safety follow-up

CI exposed six 3.13-only test failures under tests/_common/,
tests/diff/, tests/grade/, tests/manifest/. Root cause: Python 3.13
changed Path.resolve() to raise OSError(errno.ELOOP) on cyclic
symlinks instead of RuntimeError. SignalForge's _canonicalise_path
and the manifest loader's path canonicalisation catch RuntimeError
only — on 3.13 the cycle now escapes as OSError or short-circuits
to a different typed error before the loop check fires.

Fixing it touches production code (signalforge/_common/path_safety.py
and signalforge/manifest/loader.py) plus four test fixtures, which is
substantial scope creep for a tooling-migration PR. Narrow the matrix
to 3.11 / 3.12 (both green: 1830 passed locally and in CI) and track
the 3.13 work in a follow-up issue.

- ci.yml: matrix python-version: ["3.11", "3.12"]; codecov-upload gate
  flips from 3.13 → 3.12 (matrix ceiling). Comment block records the
  reason and points at the follow-up.
- CLAUDE.md / CONTRIBUTING.md / docs/cli-ops.md / python-build.md /
  ci-supply-chain.md: every "3.11 / 3.12 / 3.13" reference now reads
  "3.11 / 3.12"; ci-supply-chain.md and python-build.md add a "3.13
  is deferred" paragraph naming the catch-site fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: address CodeRabbit + Copilot review feedback

Five fixes from PR #95 review (all real issues, no false positives):

- `.claude/rules/ci-supply-chain.md`: codecov-gate example said
  `matrix.python-version == '3.13'` (stale from the 3.13 matrix);
  current CI gates on `'3.12'`. Updated and noted the flip-back when
  issue #96 lands. [coderabbit/copilot — same finding, threads 1+3]
- `.github/workflows/ci.yml`: `actions/checkout` now sets
  `persist-credentials: false` — CI is read-only; least-privilege
  for the GITHUB_TOKEN. [coderabbit — thread 4]
- `.github/workflows/publish.yml`: same `persist-credentials: false`
  on both `Checkout` steps; same justification (build job doesn't
  push to git). [coderabbit — thread 5]
- `.github/workflows/publish.yml`: `astral-sh/setup-uv` now pins
  `python-version: "3.11"` on both jobs for release-build
  reproducibility. Without it, `uv build` picks whatever
  `ubuntu-latest` happens to ship. [copilot — thread 2]
- `docs/cli-ops.md`: clarify that the `pip install -e ".[dev]"`
  fallback omits the `build` package (uv-only delta in
  `[dependency-groups].dev` powering wheel_smoke). Pip users
  shouldn't assume identical tooling coverage. [coderabbit — thread 6]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: publish MkDocs Material site to GitHub Pages on push to main (#97)

* docs: publish MkDocs Material site to GitHub Pages on push to main

Mirrors clauditor's docs-publishing setup. The published site at
https://wjduenow.github.io/SignalForge/ redeploys on every push to
main; the gh-pages branch is generated, not authored.

- `mkdocs.yml`: Material theme with light/dark toggle, search +
  include-markdown plugins, mermaid via pymdownx.superfences,
  exclude_docs: research/ (internal analysis stays off the site),
  edit_uri: edit/dev/docs/ (doc edits land on dev like every other
  PR; main is the release line). Nav: Home + CLI Reference +
  Pipeline Stages (manifest/warehouse/safety/draft/prune/grade/diff)
  + Audits & Sidecars + e2e + coverage.
- `docs/index.md`: 4-line include-markdown stub pulling in the root
  README. Mirrors clauditor verbatim — README stays the canonical
  authored home doc; site home auto-syncs on every build.
- `.github/workflows/ci.yml`: new `docs:` job gated on
  `github.ref == 'refs/heads/main' && github.event_name == 'push'`
  with job-scoped `permissions: contents: write` (workflow default
  stays `contents: read`). Uses pinned `astral-sh/setup-uv@v8.1.0`
  with `python-version: "3.11"` (matches publish.yml floor); runs
  `uv run mkdocs gh-deploy --force --no-history`. Deliberately does
  NOT set `persist-credentials: false` on the checkout step —
  `gh-deploy` needs the persisted GITHUB_TOKEN to push to gh-pages.
- `pyproject.toml`: add `mkdocs-material>=9.0` and
  `mkdocs-include-markdown-plugin>=6.0` to `[dependency-groups].dev`
  only. Pip contributors don't need to build docs; this stays a
  uv-only delta (alongside `build` for wheel_smoke).
- `.gitignore`: ignore `site/` (mkdocs build output).
- `README.md`: docs badge top-of-file.
- `CLAUDE.md` / `CONTRIBUTING.md`: one-liner pointing at the
  published site and the deploy contract.
- `.claude/rules/docs-publishing.md` (new): full deploy contract —
  trigger rule, include-markdown pattern, edit_uri choice,
  persist-credentials exception, exclude_docs convention, local
  build recipe, when to update mkdocs.yml vs the docs themselves,
  first-time GH Pages setup the maintainer does once.

Local validation: `uv run mkdocs build` succeeds (warnings on
repo-internal links to plans/super/ and .claude/rules/ are expected
and matched in clauditor — those aren't part of the published
site). `uv run ruff check .` / `ruff format --check .` clean.
1823/1823 tests pass on 3.13 (6 pre-existing #96 failures
unrelated).

Post-merge maintainer step (once): Settings → Pages → source =
"Deploy from a branch", branch = gh-pages, folder = / (root). The
first push to main after this merges lands the gh-pages branch;
toggle Pages on right after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address PR #97 review — split docs dep group + PR-time build gate

Two Copilot suggestions, both real wins:

- Extract `[dependency-groups].docs` (mkdocs-material +
  mkdocs-include-markdown-plugin only). Both CI docs jobs now
  `uv run --only-group docs ...` so they pull just MkDocs + plugins,
  not the heavy dev set (dbt-core, pyright, pytest). `dev` includes
  `docs` via `{include-group = "docs"}` so `uv sync --dev` still
  gives contributors everything. [thread: ci.yml deploy pulled --dev]
- Add a read-only `docs-build` job that runs on every PR + push
  (`uv run --only-group docs mkdocs build`, no write perms). Catches
  a broken mkdocs.yml / plugin config / include-markdown syntax error
  at PR time instead of silently merging and only failing the
  post-merge gh-pages deploy. [thread: no pre-merge docs build signal]

The third thread (Home page "Edit this page" link points at the
docs/index.md include stub rather than README.md) is an accepted
limitation of the include-markdown pattern — Material has no per-page
edit-button hide without a template override, and clauditor's
identical setup accepts it. The stub a contributor lands on literally
documents where the real content lives. Documented in the review
summary; no code change.

`.claude/rules/docs-publishing.md` updated: two-job structure, the
docs dependency-group split, and the "new plugin goes in docs not
dev" convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release-manager: cut TestPyPI from dev, PyPI from main + uv refresh

Branch discipline: test releases are now cut from `dev` (was "any
branch") and full releases from `main` (unchanged). The publish.yml
prerelease-flag routing is unchanged — it's the mechanism; the branch
is the maintainer discipline the skill enforces in pre-flight. dev is
the in-development line feeding TestPyPI; main is the released line
feeding PyPI.

Also refreshes the skill for the uv migration (PRs #95/#97):
- validation command → `uv sync --dev && uv run ruff/pyright/pytest`
- `python -m build` → `uv build` (both build blocks + PR-body string)
- allowed-tools gains `Bash(uv *)`; compatibility frontmatter updated
- left the TestPyPI clean-room `pip install` smoke test as-is (it
  deliberately installs the published artifact in a throwaway venv)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump to 0.1.0rc3 for test release

* release-manager: pin clean-room smoke test to Python >=3.11

The Step 5 TestPyPI smoke test used a bare `python -m venv`, which
picks up whatever `python3` is on PATH — on a host where that's 3.10,
pip silently ignores the new release (requires-python >=3.11) and only
the pre-floor 0.1.0rc1 installs, producing a confusing "Could not find
a version that satisfies the requirement" right after a green publish.

Surfaced cutting v0.1.0rc3: the first clean-room attempt used the
system 3.10 and rejected rc3/rc2 as "Requires-Python >=3.11".

Fix: provision the interpreter explicitly via `uv venv --python 3.11`
(the requires-python floor — verifies installability on the minimum
supported version; uv auto-fetches 3.11 if absent) and `uv pip install
--python <venv>`. Also documents how to distinguish this from the
separate TestPyPI Fastly-cache propagation lag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: disable setup-uv cache (enable-cache: false) on all steps

CodeRabbit flagged on PR #98: setup-uv's default persists the uv cache
to the GitHub Actions Cache. On a public repo, pull_request runs share
a cache scope with the base branch, so a fork PR can poison an entry a
later trusted run restores — a cross-trust-boundary cache-poisoning
vector. Set `enable-cache: false` explicitly on every setup-uv step:
ci.yml (lint-test, docs-build, docs-deploy) + publish.yml (testpypi,
pypi). Runs are short enough that the lost cache is negligible; the
publish/deploy steps especially must never restore a poisoned cache
into a published artifact or the gh-pages deploy.

Documents the convention in .claude/rules/ci-supply-chain.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: begin 0.2.0.dev0 (#100)

* docs: README install instructions for the live PyPI release

signalforge-dbt 0.1.0 is on PyPI, so lead the install with
`pip install signalforge-dbt` instead of the clone+uv-sync path.

- Status line: "Not yet on PyPI" → "Live on PyPI — pip install signalforge-dbt".
- Quick start §1: pip install first + `signalforge --version` verify; note
  the distribution-vs-import name; add `uv tool install` / `pipx` as the
  isolated-CLI alternative; demote the clone + `uv sync --dev` path to a
  contributing pointer at CONTRIBUTING.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #104: ingest external dbt schema.yml tests into CandidateSchema (#106)

* Add super plan for #104: ingest external dbt schema.yml tests

Library-only seam (signalforge.ingest.read_schema -> IngestResult).
prune-existing CLI subcommand split to fast-follow #105.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mark #104 plan published (PR #106)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Devolve #104 plan to beads (epic bd_1-scaffolding-ky3, 8 tasks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.1: scaffold signalforge.ingest package + IngestError hierarchy + exit-code lockstep

US-001 of issue #104. Adds the signalforge.ingest subpackage scaffold and
its six-class typed-error hierarchy (DEC-001), wired into the CLI four-tier
exit-code taxonomy and the 7th AST scan.

- src/signalforge/ingest/__init__.py: package init re-exporting the
  IngestError surface.
- src/signalforge/ingest/errors.py: IngestError base (remediation +
  ↳ Remediation rendering, repr-safe values) plus five concretes —
  IngestSchemaNotFoundError / IngestSchemaParseError /
  IngestSchemaTooLargeError (tier 1) and IngestModelNotFoundError /
  IngestAnchorContractError (tier 2).
- cli/_helpers.py: every concrete mapped in _EXCEPTION_TO_EXIT_CODE at its
  tier; IngestError registered as fallback tier-1 (dual-registration).
- tests/test_audit_completeness.py: IngestError added to
  _EXCEPTION_MAPPING_EXCLUDED_BASES; Scan-7 module count bumped 10 -> 11.
- tests/cli/test_exit_codes.py: synthetic-construction branches for the
  (size, limit) and (violations,) constructor shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.2: typed ingest result models (IngestResult, SkippedTest, SkipReason)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.4: anchor-contract validator (fail-loud, collect-all)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.3: dbt test-entry parser (pure mapping)

Pure parse_test_entry(entry, *, column) mapping a single dbt schema.yml
test entry to a CandidateTest or a structured SkippedTest. Handles bare
strings, single-key dicts with inline and arguments:-nested args
(DEC-006), config-key tolerance, malformed-supported skips, custom /
namespaced skips, and best-effort ref()/source() unwrap in
relationships.to (DEC-009). No logging, no I/O, not part of the public
surface. 19-test matrix added (TDD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.5: read_schema orchestrator

Implement the public ingest entry point read_schema(schema, model, *,
project_dir=None) -> IngestResult tying together the parser, anchor
validator, and result models. Steps: str-vs-Path input contract
(Path = file, symlink-hardened via _common.path_safety; str = raw YAML),
size-cap before yaml.safe_load (DEC-005), model-block selection by name
(DEC-006), union+dedupe tests/data_tests (DEC-008), description="" default
(DEC-010), whole-file anchor check. No logging (stage-0 reader). Re-export
read_schema from the package __init__ as the public surface.

Adds the dbt-codegen-shaped fixture and 13 tests: happy path (supported +
skipped tests, dedupe, ref() unwrap, both test keys), str-content input,
and each error path (model-not-found, malformed YAML, oversize, anchor
violation, missing path), plus the disabled-prune acceptance bonus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.6: docs/ingest-ops.md operational reference + mkdocs nav

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.7: Quality gate — fix bugs from code review

- parser: model-level supported test (column=None) now routes to a
  structured malformed skip instead of raising a Pydantic ValidationError
  out of read_schema.
- reader: accepted_values dedupe key now sorts values so reordered value
  sets collapse (DEC-008 "sorted-args" contract).
- parser: clarify _extract_args docstring for non-dict `arguments`.
- tests: regression coverage for both fixes (+3 ingest tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ky3.8: Patterns & Memory — ingest-layer rule + CLAUDE.md + cli-layer count

- New .claude/rules/ingest-layer.md distilling the #104 conventions
  (str-vs-Path contract, fail-loud anchor vs skip-record taxonomy,
  no-logging stage-0, safe-YAML size cap, dbt syntax tolerance,
  11th errors.py exit-code lockstep, deferred #105 CLI).
- CLAUDE.md: #104 shipped-issue entry + public-API surface entry.
- cli-layer.md: scan-7 count 10->11; IngestError joins DemoError as a
  span-tier base (frozenset-only, no fallback) — and aligned _helpers.py
  by removing the IngestError tier-1 fallback to match that precedent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #104: Address PR review feedback (Copilot)

- errors.py: correct IngestError docstring — base is frozenset-only (no
  fallback tier), concretes span tiers 1 and 2 like DemoError.
- __init__.py: refresh module docstring — the full library seam (reader,
  parser, anchor, models) ships in #104, not "typed-error surface only".
- parser.py: _extract_args now drops the structural `arguments` key from
  inline args so a non-mapping `arguments:` can't leak (+ regression test).
- docs/ingest-ops.md: fix usage snippet to use WarehouseAdapter.from_profile
  (the public factory; there is no top-level from_profile) and pass an
  un-entered adapter to prune_tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #105: signalforge prune-existing CLI subcommand (#107)

* Add super plan for #105: signalforge prune-existing CLI subcommand

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mark plan phase published (PR #107)

* Mark plan devolved; record beads manifest (epic bd_1-scaffolding-ad1)

* bd_1-scaffolding-ad1.1: Hoist bare-name model resolver to _helpers

#105 US-001 / DEC-008. Move the bare-name model resolver from lint.py to
signalforge.cli._helpers as _resolve_model_by_key(manifest, key) -> Model so
the future prune-existing subcommand shares one resolver rather than
copy-pasting the body. lint.py keeps a thin _resolve_model_for_lint wrapper
that delegates. Identical behaviour: unique_id/file-path via
Manifest.get_model; bare-name via iter_models with multi-match
disambiguation + disabled-model exclusion + the same ModelNotFoundError
messages. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.2: Add prune-existing add_parser + stub handler

US-002 of #105 — contract surface (DEC-001, DEC-002, DEC-003). Adds
src/signalforge/cli/prune_existing.py with add_parser registering the
prune-existing subcommand and full flag set (positional <model>,
required --schema, --project-dir/--manifest/--profiles-dir,
--scope/--sample-strategy/--format choice flags, --dry-run, and the
--quiet/--verbose/--no-color observability triad). Drops --mode/--write/
--min-score/--select/--estimate per DEC-002/DEC-003. cmd_prune_existing
is a stub returning 0 (full ingest->prune->diff body lands in US-003).

Registered in signalforge.cli.__init__._build_parser. Help strings are
surface 1 of the 5-surface parity contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.5: Add prune-existing --help subprocess smoke

Extends the @pytest.mark.cli_subprocess gate to the prune-existing
subcommand (#105 US-005), mirroring the existing generate/lint/version/
init-demo --help smokes. Asserts returncode 0, the unique --schema flag
in stdout, and the no-traceback floor on stderr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.3: Implement cmd_prune_existing orchestrator (ingest -> prune -> diff)

US-003 of #105. Replaces the US-002 stub with the real ingest -> prune ->
diff orchestrator (no draft, no grade, no LLM call), inside one
try/except Exception boundary (DEC-016 — no traceback ever leaks).

- Resolve project_dir (mirror generate: --project-dir absolute assertion,
  walk-up default); --no-color/--profiles-dir env mutation (DEC-023).
- Resolve <model> via _resolve_model_by_key (bare-name / unique_id /
  file-path). Canonicalise --schema via canonicalise_user_path (DEC-005).
- read_schema(Path, model, project_dir=...) so ingest typed errors fire
  and route via the pre-registered exit-code table (DEC-006 — no bespoke
  CliPruneExisting* wrappers).
- Skipped-test report to stderr: grouped summary + --verbose per-item
  detail, suppressed by --quiet (DEC-007). print_stderr, not _LOGGER.
- PruneConfig --scope/--sample-strategy overrides via model_validate
  (DEC-002). _make_warehouse_adapter test seam (DEC-009); prune_tests owns
  the with-adapter block (un-entered adapter).
- Feed the external schema.yml text as render_diff existing_schema with
  grading_report=None -> kept/kept-uncertain/dropped, never flagged
  (DEC-004); --dry-run -> write_sidecar=False. render_to_text -> stdout.
- 3-stage progress (1/3 ingest, 2/3 prune, 3/3 diff) via the existing
  emit_progress_* helpers, generalised with a total= kwarg (default 5).

Adds an Austin-aligned schema.yml fixture whose four supported test types
reference real stg_bikeshare_trips columns plus two unsupported tests for
the skip path. Tests cover happy path, skipped report (+ --verbose +
--quiet), each error-path exit-code tier with a no-traceback floor,
--dry-run no-disk, --format json, and bare-name vs unique_id resolution,
via a FakeBigQueryClient-backed BigQueryAdapter on the patch seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.4: Add cli-ops prune-existing section + 5-surface parity test

US-004 of #105. Documents the `signalforge prune-existing` subcommand in
docs/cli-ops.md (synopsis, flag-reference table, four-tier exit codes,
skipped-test summary stderr shape, read-only/no-`--write` note, the
unified-diff-against-your-file framing + cosmetic-reformatting caveat,
the no-grading -> never-flagged note, and a worked example). Bumps the
subcommand count from four to six.

Adds tests/cli/test_5_surface_parity_prune_existing.py mirroring
test_5_surface_parity_select.py: asserts the flag set (--schema, --scope,
--sample-strategy, --dry-run, prune-existing) and the read-only intent
appear consistently across argparse help, prune_existing.py help/docstrings,
docs/cli-ops.md, and the plan's DEC list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.6: Quality gate — fix bugs from code review

- prune engine: short-circuit empty candidate before warehouse contact
  (all-skipped schema.yml no longer materialises a temp table for zero
  tests); regression test on the default materialised+sample path.
- prune-existing: scrub \n/\r/\t in the --verbose skipped-test report so
  an operator YAML value can't inject a fake bullet line (mirrors
  format_batch_summary's control-char scrub).
- test: assert the exact kept/dropped mix (7/3) in the --format json test
  so a prune keep-everything/drop-everything regression fails loudly
  (testing-signal.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.6: Quality gate — fix CodeRabbit finding (plan ref)

Correct the prune-existing subprocess-smoke docstring to cite
plans/super/105-prune-existing-cli.md (was 104).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bd_1-scaffolding-ad1.7: Patterns & Memory — record #105 conventions

- ingest-layer.md: prune-existing shipped (was deferred); record the
  no-bespoke-wrappers (DEC-006), --mode-inert (DEC-002), read-only
  (DEC-003/004) findings.
- cli-layer.md: bare-name resolver hoist done in #105; new section on
  "library errors already mapped -> no per-class wrappers" + no-LLM-stage
  flag-audit + read-only conventions.
- prune-engine.md: empty-candidate short-circuit (no warehouse contact).
- CLAUDE.md: add #105 to shipped issues + CLI surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #105: Address PR review feedback (Copilot + CodeRabbit)

- prune_existing.py: refresh stale module docstring (no longer a "stub";
  the orchestrator is implemented) and note the no-bespoke-wrappers seam.
- prune/engine.py: call _maybe_emit_kept_rate_warning at the new
  empty-candidate return site (no-op via total==0 early-return) to honour
  the "every return site" contract (#51).
- docs/cli-ops.md: subcommand count six -> five; avoid a line starting
  with "#104" (heading parse); trim leading spaces inside the Remediation
  inline code span.
- plans/super/105-prune-existing-cli.md: markdownlint — language on the
  fenced block, avoid "#104" line start.

--profiles-dir non-canonicalisation is intentional (mirrors generate;
dbt places profiles.yml outside the project) — documented as a false
positive on the thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: surface ingest + prune-existing (#104/#105) in user docs (#108)

The external-test ingest layer (#104) and the prune-existing CLI (#105)
only appeared in their own ops pages; the README — the published home
page and product pitch — never mentioned them, and ingest-ops.md still
said the CLI didn't exist.

README:
- "Why this exists" / "What it does": SignalForge now prunes ANY
  generator's tests, not just its own drafts.
- "How it works": add the second (no-LLM) entry path diagram.
- New "Prune the tests you already have" cookbook with a runnable
  example + an availability callout (prune-existing is the v0.2 dev
  line, not yet on the v0.1 PyPI release; install-from-source command).
- CLI list: four -> five subcommands; prune-existing flags; v0.2 marker.
- Roadmap: note prune-existing on the v0.2 row.

Docs:
- ingest-ops.md: fix stale "no prune-existing CLI yet" wording (it
  shipped in #105); cross-link the CLI reference.
- cli-ops.md: cross-link the supported/skipped test taxonomy in
  ingest-ops.md.

Verified: mkdocs build clean; all in-page anchors resolve.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: Python 3.13 compatibility for the path-safety layer (#109)

* #96: Python 3.13 compatibility for the path-safety layer

Python 3.13 (gh-108958) changed `Path.resolve()` to raise
`OSError(errno.ELOOP)` instead of `RuntimeError` on cyclic symlinks, and
made `strict=False` resolution stop raising on a cycle entirely (it returns
a best-effort unresolved path). The three loop-guard sites caught only
`RuntimeError`, so on 3.13 a symlink cycle either escaped as a raw OSError
or slipped silently past the containment check.

Fix every loop-guard site to resolve the input path `strict=True` first
(falling back to `strict=False` only for a genuinely missing target) and to
catch both `RuntimeError` (<= 3.12) and `OSError(errno.ELOOP)` (>= 3.13):

- `signalforge/_common/path_safety.py`
- `signalforge/manifest/loader.py::_canonicalise_path`
- `signalforge/demo/__init__.py::copy_demo`

The demo test's symlink-cycle case no longer skips on Linux/WSL2 and now
asserts the cause is `RuntimeError | OSError`.

Re-add 3.13 to the CI matrix as the ceiling; flip the codecov upload gate
3.12 -> 3.13. Update the deferral prose in CLAUDE.md, CONTRIBUTING.md,
python-build.md, and ci-supply-chain.md.

Validated on 3.11 / 3.12 / 3.13: 1923 passed; ruff + format + pyright clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: keep the manifest non-dict-root guard live (pyright unreachable fix)

`raw: dict[str, Any] = json.load(fh)` asserted a type json.load doesn't
guarantee, so pyright treated the `isinstance(loaded, dict)` root-shape
guard as dead code. Load as `Any`, narrow to `dict[str, Any]` after the
check so the guard stays live. Add a test (`non_dict_root.json` — valid
JSON array root) that exercises the now-reachable branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: narrow strict-resolve fallback to missing-target only (PR review)

Copilot review: the `except OSError` fallback to `resolve(strict=False)`
was too broad — it downgraded ANY non-ELOOP OSError (e.g. PermissionError)
to best-effort resolution, contradicting the "fallback only for a missing
target" intent and potentially weakening the containment check.

Narrow the fallback to `FileNotFoundError` / `NotADirectoryError`; re-raise
every other OSError. Applied to all three loop-guard sites
(_common/path_safety, manifest/loader, demo). Tighten the demo docstring to
state the strict-then-fallback behaviour explicitly. Add a regression test
asserting a PermissionError on the input path propagates rather than being
swallowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #96: close codecov patch-coverage gaps on the path-safety fix

The codecov upload runs from the 3.13 leg, so the `except RuntimeError`
arms (which only execute on Python <= 3.12) and the new non-ELOOP re-raise
arms showed as uncovered. Address honestly:

- Add regression tests for the non-ELOOP re-raise arms (PermissionError on
  the input path AND project_dir for path_safety; input path for loader;
  dest for demo) — these encode the Copilot fix and are reachable on every
  interpreter.
- `# pragma: no cover` the `except RuntimeError` arms (<=3.12-only cycle
  signal; exercised on the 3.11/3.12 CI legs, structurally unreachable on
  the 3.13 upload leg) and loader's project_dir handler block (defensive —
  `load()` passes an already-resolved project_dir, mirroring the tested
  _common.path_safety helper).

Patch coverage on the touched files is now 100% on the 3.13 upload leg.
Validated on 3.11 / 3.12 / 3.13: 1928 passed; ruff + format + pyright clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: steer pre-release gated-marker runs to the matrix ceiling (3.13) (#110)

The gated markers (bigquery / anthropic / cli_subprocess / e2e / wheel_smoke)
carry no Python-version pin and run on whatever interpreter `uv run` resolves;
no CI job runs them. Following #96 (3.13 is now the matrix ceiling), document
that maintainers should run the packaging/entry-point markers (wheel_smoke,
cli_subprocess) under `uv run --python 3.13`, while the interpreter-invariant
live-service markers stay single-version to avoid multiplying paid API calls.

CONTRIBUTING.md § Pre-release coverage audit + testing-signal.md § Known gap.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: release 0.2.0

* test: close 0.2.0 patch-coverage gaps in ingest + prune-existing (#112)

* test: close 0.2.0 patch-coverage gaps in ingest + prune-existing

Brings ingest/parser.py and ingest/reader.py to 100% and
cli/prune_existing.py to 96% on the default (non-gated) test run,
raising the 0.2.0 release PR's codecov patch coverage from 92.44%.

- parser: ref()/source() non-quoted + arity edges, non-dict bodies,
  multi-key + non-str/dict entries, model-level relationships skip
- reader: path-containment failure, unreadable file (POSIX, non-root),
  non-dict / nameless column entries ignored
- prune-existing: walk-up project-dir resolution (success + failure),
  --no-color env mutation, empty skipped-report guard

Residual: prune_existing.py:270 (real WarehouseAdapter.from_profile,
gated-only) and :447-454 (--profiles-dir resolve error, flaky to
trigger) are documented coverage-policy exclusions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: assert diff.json sidecar durability in prune-existing success tests

Addresses CodeRabbit review on #112: the three new success-path tests
now also assert the default-on .signalforge/diff.json sidecar is
written (DEC-016 e2e convention — exit 0 + no traceback + durable
sidecar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit findings on ingest + prune-existing (#104/#105) (#113)

#3 (ingest reader): enforce the DEC-005 size cap from stat().st_size
BEFORE reading file bytes for a Path input, so an oversize schema.yml is
rejected without first being slurped into memory. The post-resolve
length check stays as the str-path enforcement + backstop.

#2 (prune-existing --dry-run docs): correct cli-ops.md — --dry-run
suppresses the .signalforge/diff.json sidecar, but the fail-closed
.signalforge/prune.jsonl audit is STILL written (every prune run leaves
a durable receipt — the cross-stage invariant; mirrors generate).
Pinned by strengthening test_dry_run_writes_no_sidecar to assert
prune.jsonl is present.

(#1, env-var restore, is left as-is — DEC-023 'mutate, don't restore'
is the deliberate, consistent pattern across all subcommands.)

reader.py back to 100% coverage; full suite 1945 passed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants