Skip to content

docs: publish MkDocs Material site to GitHub Pages on push to main - #97

Merged
wjduenow merged 2 commits into
devfrom
feature/docs-publishing
May 19, 2026
Merged

docs: publish MkDocs Material site to GitHub Pages on push to main#97
wjduenow merged 2 commits into
devfrom
feature/docs-publishing

Conversation

@wjduenow

@wjduenow wjduenow commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Second of two PRs mirroring clauditor's setup. PR #95 landed uv tooling; this one adds the docs publishing.

  • Trigger: push to main only. The docs: job in .github/workflows/ci.yml is gated on github.ref == 'refs/heads/main' && github.event_name == 'push'. Dev pushes don't redeploy — the published site reflects the released line.
  • Site: https://wjduenow.github.io/SignalForge/ (live after the first dev → main merge + Settings → Pages flip).
  • Home page: docs/index.md is a 4-line include-markdown stub pulling in README.md. README stays the canonical authored home; the site auto-syncs on every build.
  • Exclusions: docs/research/ is exclude_docs:-gated. Internal analysis (dbt-pain-deep-dive, dbt-research-index, etc.) stays off the public site.
  • Edit links: edit_uri: edit/dev/docs/ — the "Edit this page" link on each page targets the dev branch (where every other PR work happens), not main.
  • Persist-credentials: the docs job's checkout deliberately does NOT set persist-credentials: falsemkdocs gh-deploy issues a real git push to gh-pages and relies on the token. Scope is bounded by permissions.contents: write at the job level (workflow default stays contents: read).

Files

  • mkdocs.yml (new) — Material theme + plugins + nav
  • docs/index.md (new) — 4-line include-markdown stub
  • .github/workflows/ci.yml — new docs: job
  • .claude/rules/docs-publishing.md (new) — full deploy contract + first-time-setup checklist
  • pyproject.toml + uv.lock — add mkdocs-material + mkdocs-include-markdown-plugin to [dependency-groups].dev (uv-only)
  • .gitignore — ignore site/
  • README.md — docs badge
  • CLAUDE.md / CONTRIBUTING.md — one-liner pointing at the published site

First-time maintainer step (one-time, post-merge)

After this lands on dev and gets merged to main, the first push to main runs the docs job, lands gh-pages, then the maintainer toggles GitHub Pages on once: Settings → Pages → Build and deployment → source: "Deploy from a branch", branch: gh-pages, folder: / (root). From then on every push to main redeploys within ~30s.

Test plan

  • uv sync --dev clean (mkdocs-material 9.7.6 + include-markdown 7.3.0 installed)
  • uv run mkdocs build succeeds — site/index.html carries the README body (verified via grep)
  • uv run mkdocs build without --strict matches clauditor's pattern; warnings on repo-internal links to plans/super/ and .claude/rules/ are expected (those aren't published)
  • uv run ruff check . / ruff format --check . clean
  • 1823/1823 pytest pass on 3.13 (6 pre-existing Python 3.13 compatibility for path-safety layer #96 failures unrelated to this PR)
  • CI green on the matrix (PR pushes don't trigger the docs job; only post-merge main pushes do)
  • After merge → main: gh-pages branch lands; toggle Pages in Settings; verify site loads

Follow-up

None directly from this PR. #96 (Python 3.13 path-safety) remains the only open follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Documentation site now available on GitHub Pages with automatic updates on pushes to main
    • Material theme with search functionality and light/dark mode support
  • Documentation

    • Added contributor guide for documentation editing and local preview workflow
    • Updated project metadata with documentation site information
  • Chores

    • Added documentation build dependencies
    • Configured CI workflow for automated site publishing

Review Change Stack

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>
@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: c017112f-926d-4449-9994-ef11247c70ff

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 establishes a complete MkDocs Material documentation site published to GitHub Pages. It adds the MkDocs configuration, deploys via a new CI job on pushes to main, includes the README as the homepage, and documents the entire publishing flow for developers and contributors.

Changes

MkDocs Documentation Publishing

Layer / File(s) Summary
MkDocs configuration and dependencies
mkdocs.yml, pyproject.toml, .gitignore
MkDocs config defines the "SignalForge" site with Material theme, Mermaid support, navigation structure, and research doc exclusions. Development dependencies add mkdocs-material>=9.0 and mkdocs-include-markdown-plugin>=6.0. Build output directory site/ is added to gitignore.
CI/CD docs publishing job
.github/workflows/ci.yml
New docs job publishes to GitHub Pages on every push to main, with write permissions for Pages deployment, Python 3.11, uv dependency sync, and mkdocs gh-deploy --force --no-history.
Documentation site homepage
docs/index.md
Homepage template includes the project README.md via Markdown include directive with rewrite-relative-urls=true to adjust links for the docs site context.
Developer and project documentation
.claude/rules/docs-publishing.md, CLAUDE.md, CONTRIBUTING.md, README.md
Publishing contract rules document CI trigger semantics, site structure conventions, local preview commands, and operational guidance. Project files add documentation site overview, update contributor guidance, and add docs status badge.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • wjduenow/SignalForge#85: Both PRs modify .github/workflows/ci.yml push-trigger and job configuration; this PR adds the docs publishing job while the other extends branch patterns to include release/*.

Poem

📚 A docs site grows from config so clear,
MkDocs Material makes content shine bright,
GitHub Pages deploys on every push here,
README becomes homepage—what a delight!
From local preview to published pages,
SignalForge docs span through the ages. 🐰

🚥 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 clearly and specifically describes the main change: adding automatic publishing of an MkDocs Material site to GitHub Pages triggered by pushes to the main branch.
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.

@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 requested a review from Copilot May 19, 2026 19:25
@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

Adds automated publishing of the repository’s MkDocs Material documentation site to GitHub Pages, aligned with the repo’s “dev → main is release” workflow (deploy only on pushes to main).

Changes:

  • Introduces MkDocs Material site configuration and a docs home page that includes the repo-root README.md.
  • Adds a docs GitHub Actions job that deploys the site to gh-pages on push to main.
  • Updates dev tooling deps (uv), contributor docs, and repo metadata (badge, ignore rules) to support docs publishing.

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.github/workflows/ci.yml Adds docs job to deploy MkDocs to gh-pages on push to main with scoped write permissions.
mkdocs.yml New MkDocs Material configuration (theme, plugins, nav, exclusions, edit links).
docs/index.md New include-markdown stub to render repo-root README.md as the docs home page.
pyproject.toml Adds MkDocs dependencies to uv’s dev dependency group (uv-only extras).
uv.lock Locks newly added MkDocs-related dependencies and transitive requirements.
README.md Adds a docs badge linking to the published GitHub Pages site.
.gitignore Ignores local MkDocs build output (site/).
CONTRIBUTING.md Points contributors to the published docs site and local preview command.
CLAUDE.md Documents the docs site deployment behavior and configuration at a high level.
.claude/rules/docs-publishing.md Adds a written “deploy contract” and maintainer setup checklist for docs publishing.

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

Comment thread mkdocs.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
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>
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary

3 Copilot threads addressed in commit 7edd828. Two fixed; one documented as an accepted limitation.

Fixed (2 items)

File Line Issue Source Fix
.github/workflows/ci.yml 102 Docs deploy ran uv sync --dev, pulling the entire dev set (dbt-core, pyright, pytest) for a job that only needs MkDocs copilot Extracted [dependency-groups].docs (mkdocs deps only); both docs jobs now uv run --only-group docs .... dev includes docs via {include-group = "docs"} so uv sync --dev is unchanged for contributors
.github/workflows/ci.yml 76 No CI signal that the site builds until a post-merge main deploy — a broken mkdocs.yml could merge cleanly copilot Added a read-only docs-build job (all PRs + pushes, no write perms) running uv run --only-group docs mkdocs build. The gh-deploy job stays gated to main

Accepted limitation (1 item)

File Line Issue Reason
mkdocs.yml 10 The Home page "Edit this page" link points at docs/index.md (the include-markdown stub), not README.md Inherent to the include-markdown home-page pattern. Material has no per-page edit-button hide without a custom template override, and a global edit_uri: "" would kill edit links on every page. clauditor's identical setup accepts this. The stub a contributor lands on is 4 lines and literally documents where the real content lives (README.md), so the link is self-explanatory rather than misleading. Not worth a template override.

CodeRabbit reported no actionable comments.

@wjduenow
wjduenow merged commit eaa9176 into dev May 19, 2026
5 checks passed
@wjduenow
wjduenow deleted the feature/docs-publishing branch May 19, 2026 23:09
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