#104: ingest external dbt schema.yml tests into CandidateSchema - #106
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (4)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rror 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>
… + IngestError hierarchy
…ppedTest, SkipReason) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-all) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
…docs nav Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
…E.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>
|
@codereabbit review |
There was a problem hiding this comment.
Pull request overview
Adds a new signalforge.ingest stage-0 reader seam that parses an externally-authored dbt schema.yml and emits a typed CandidateSchema (plus structured skip records) consumable by prune_tests, enabling pruning of tests produced outside the LLM draft layer.
Changes:
- Introduces
signalforge.ingest.read_schema(...) -> IngestResultwith safe YAML loading (size cap +yaml.safe_load), model selection, test parsing, dedupe, and anchor-contract validation. - Adds typed ingest models (
IngestResult,SkippedTest,SkipReason) and a full ingest typed-error hierarchy wired into CLI exit-code mapping + audit completeness scans. - Adds comprehensive unit tests + fixtures, plus ops/docs and repo “patterns & memory” updates (MkDocs nav, rules, CLAUDE.md, plan doc).
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/signalforge/ingest/__init__.py |
Public ingest API re-exports. |
src/signalforge/ingest/errors.py |
Ingest typed error hierarchy + remediation strings. |
src/signalforge/ingest/models.py |
IngestResult / SkippedTest models and skip taxonomy. |
src/signalforge/ingest/parser.py |
Pure dbt test-entry parsing + ref()/source() unwrap. |
src/signalforge/ingest/reader.py |
Orchestrator: input resolution, size cap, YAML parse, build candidate, dedupe, anchor validation. |
src/signalforge/ingest/anchor.py |
Collect-all anchor-contract validator raising ingest-typed error. |
src/signalforge/cli/_helpers.py |
Registers ingest errors into exit-code tiers. |
tests/test_audit_completeness.py |
Updates AST-scan expectations for new ingest stage/errors module. |
tests/cli/test_exit_codes.py |
Adds ingest exception construction edge shapes for exit-code tests. |
tests/ingest/test_models.py |
Validates ingest model contracts (frozen/extra ignore/repr/etc). |
tests/ingest/test_parser.py |
Parser shape matrix coverage. |
tests/ingest/test_anchor.py |
Anchor-contract collect-all behavior tests. |
tests/ingest/test_reader.py |
End-to-end reader tests + fixture usage + prune-disabled acceptance check. |
tests/fixtures/ingest/schema_codegen_shaped.yml |
dbt-codegen-shaped schema fixture used by ingest tests. |
docs/ingest-ops.md |
Operator-facing ingest ops reference and usage examples. |
mkdocs.yml |
Adds ingest ops doc to navigation. |
plans/super/104-ingest-external-tests.md |
Captures design decisions and implementation plan. |
CLAUDE.md |
Updates shipped-issues list and public API surface. |
.claude/rules/ingest-layer.md |
Codifies ingest-layer conventions/patterns for future work. |
.claude/rules/cli-layer.md |
Updates exit-code scan docs for the new ingest stage/base. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/ingest/test_reader.py (1)
222-255: ⚡ Quick winMark the disabled-prune integration test as gated.
This test spans ingest + prune integration behavior and should be explicitly marker-gated to align with the suite’s default test selection controls.
Suggested change
+@pytest.mark.e2e def test_produced_candidate_is_accepted_by_disabled_prune(tmp_path: Path) -> None:As per coding guidelines,
**/test_*.py: Configure pytest markers for gated tests (e.g.,@pytest.mark.e2e) and register them inpyproject.tomlwithaddoptsexclusions by default.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/ingest/test_reader.py` around lines 222 - 255, Mark the integration test function test_produced_candidate_is_accepted_by_disabled_prune with a gated pytest marker (e.g., add a leading import pytest and decorate the function with `@pytest.mark.e2e`) so it’s excluded from the default unit test run; then register that marker in the project test config (pyproject.toml under tool.pytest.ini_options.markers or pytest.ini) and update default addopts to exclude the marker (e.g., -m "not e2e") so gated tests are run only when explicitly requested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/signalforge/ingest/errors.py`:
- Around line 93-95: Remove the IngestError.__str__ override so error rendering
remains centralized in the CLI sink; locate the IngestError class in
src/signalforge/ingest/errors.py and delete the def __str__(self) -> str: method
(the two-line method returning f"{self.message}\n ↳ Remediation:
{self.remediation}"), leaving the default Exception behavior intact so
format_error_to_stderr (the CLI handler) controls formatting.
---
Nitpick comments:
In `@tests/ingest/test_reader.py`:
- Around line 222-255: Mark the integration test function
test_produced_candidate_is_accepted_by_disabled_prune with a gated pytest marker
(e.g., add a leading import pytest and decorate the function with
`@pytest.mark.e2e`) so it’s excluded from the default unit test run; then register
that marker in the project test config (pyproject.toml under
tool.pytest.ini_options.markers or pytest.ini) and update default addopts to
exclude the marker (e.g., -m "not e2e") so gated tests are run only when
explicitly requested.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5309f532-7522-41f1-a3a9-3bd817365caa
📒 Files selected for processing (20)
.claude/rules/cli-layer.md.claude/rules/ingest-layer.mdCLAUDE.mddocs/ingest-ops.mdmkdocs.ymlplans/super/104-ingest-external-tests.mdsrc/signalforge/cli/_helpers.pysrc/signalforge/ingest/__init__.pysrc/signalforge/ingest/anchor.pysrc/signalforge/ingest/errors.pysrc/signalforge/ingest/models.pysrc/signalforge/ingest/parser.pysrc/signalforge/ingest/reader.pytests/cli/test_exit_codes.pytests/fixtures/ingest/schema_codegen_shaped.ymltests/ingest/test_anchor.pytests/ingest/test_models.pytests/ingest/test_parser.pytests/ingest/test_reader.pytests/test_audit_completeness.py
- 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>
PR Review SummaryAll review threads addressed in Fixed (5 items)
False Positives (1 item)
CI green (lint-test 3.11/3.12, docs-build, CodeRabbit). Full suite: 1878 passed; the only failures are the 6 pre-existing WSL2/Python-3.13 symlink-loop tests (issue #96), unrelated to this PR. |
* 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>
* 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>
Summary
Super plan for #104 — an adapter that parses an external dbt
schema.ymland emits a typedCandidateSchemaconsumable byprune_tests, so SignalForge can prune any generator's tests, not just its own LLM drafts.Phase: detailing (awaiting approval)
Scope: library seam only (
signalforge.ingest.read_schema -> IngestResult). Theprune-existingCLI subcommand split to fast-follow #105 (blocked on this issue).Stories: 6 implementation + Quality Gate + Patterns & Memory
Decisions: 11 captured (DEC-001 … DEC-011)
Key decisions
signalforge.ingestsubpackage (11th per-stageerrors.py; AST-scan + exit-code lockstep)IngestResultskip recordssignalforge prune-existingCLI subcommand (prune external schema.yml tests) #105tests:+data_tests:, inline +arguments:args, union + deduperef()/source()unwrap inrelationships.todescriptiondefaults to""when absentPlan document
See
plans/super/104-ingest-external-tests.mdfor the full plan (discovery, architecture review, decisions, story breakdown).Next steps
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores