Skip to content

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

Merged
wjduenow merged 1 commit into
devfrom
docs/ingest-prune-existing
May 21, 2026
Merged

docs: surface ingest + prune-existing (#104/#105) in user-facing docs#108
wjduenow merged 1 commit into
devfrom
docs/ingest-prune-existing

Conversation

@wjduenow

Copy link
Copy Markdown
Owner

Summary

#104 (external-test ingest layer) and #105 (signalforge prune-existing CLI) are meaningful feature additions, but they only surfaced in their own ops pages. The README — the published home page and the canonical product pitch — never mentioned them, and docs/ingest-ops.md was stale (still said the CLI "doesn't exist yet"). This PR brings the user-facing docs up to date.

Changes

README.md (product story — Tier 1)

  • "Why this exists" + "What it does": SignalForge now prunes any generator's tests (dbt-codegen / dbt Copilot / DinoAI / datapilot / hand-written), not just its own LLM drafts.
  • "How it works": added the second, no-LLM entry-path diagram (existing schema.yml → prune → diff).
  • New "Prune the tests you already have" cookbook section with a runnable example, the read-only/no---write behavior, and the skipped-test reporting.
  • CLI list: four → five subcommands; added prune-existing + its flags.
  • Roadmap: noted prune-existing on the v0.2 row.

docs/ingest-ops.md (stale-doc fix — Tier 2)

  • Replaced the "no prune-existing CLI yet" note with a pointer to the shipped subcommand.
  • Cross-linked the CLI reference.

docs/cli-ops.md (cross-link — Tier 2)

  • Linked the prune-existing section to the ingest layer's supported/skipped test taxonomy.

Availability accuracy

prune-existing/ingest are on the v0.2 development line (0.2.0.dev0); the current PyPI release is v0.1.0. Since the README's quick start anchors on pip install signalforge-dbt (which installs v0.1), the new content is explicitly marked v0.2 and the cookbook carries an availability callout with an install-from-source command — so a reader following the pip path isn't misled.

Verification

  • mkdocs build clean (no errors); all in-page anchors resolve (verified against the built HTML — caught and fixed a --schema slug mismatch).
  • ruff clean; the 5-surface parity + prune_existing tests pass.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented May 21, 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: 18730988-63f9-4287-a6ab-cdb533c7ca14

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

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 merged commit 8713730 into dev May 21, 2026
5 checks passed
@wjduenow
wjduenow deleted the docs/ingest-prune-existing branch May 21, 2026 20:21
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.

2 participants