Skip to content

47: signalforge init-demo subcommand (Austin fixture for PyPI users) - #78

Merged
wjduenow merged 24 commits into
devfrom
feature/47-init-demo
May 12, 2026
Merged

47: signalforge init-demo subcommand (Austin fixture for PyPI users)#78
wjduenow merged 24 commits into
devfrom
feature/47-init-demo

Conversation

@wjduenow

@wjduenow wjduenow commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Ships #47signalforge init-demo subcommand that copies the bundled Austin demo out of the installed wheel for PyPI users. Replaces the broken cp -r tests/fixtures/... README snippet that assumed a clone but appeared under the PyPI quickstart.

Phase: complete (epic + 9 stories + Quality Gate + Patterns & Memory all closed; QG ran 4 review passes inline + addressed external PR review comments).
Stories: 9 implementation + Quality Gate + Patterns & Memory.
Decisions: 15 DECs locked at plan-time + 6 QG-pass addenda recorded in plans/super/47-init-demo.md § "Quality-gate addendum".

Highlights

  • Two-tree fixture (DEC-008 + DEC-015): src/signalforge/_demo/ ships in the wheel; tests/fixtures/dbt_project_austin/ stays the e2e fixture. Parity test asserts byte-equality except for two documented rewrites (profiles.yml, .gitignore). regenerate.sh updates both in lockstep.
  • Hatch include directive (DEC-002) + wheel_smoke maintainer gate (DEC-003): explicit include = ["src/signalforge/_demo"] in pyproject.toml; @pytest.mark.wheel_smoke test builds the wheel and inspects the artifact (run via pytest -m wheel_smoke --no-cov).
  • Public signalforge.demo.copy_demo(...) (DEC-012): library entry point with its own DemoError hierarchy; CLI wraps at the handler boundary into four tier-mapped CliInitDemo*Error wrappers.
  • --force semantics (DEC-001): atomic rmtree + copytree, refuses if dest resolves to /, ~, cwd, OR is a symlink (the symlink case is a QG follow-up — a symlink dest under --force would rmtree() the link's resolved target, an arbitrary external location).
  • env_var('GOOGLE_CLOUD_PROJECT') in shipped profile (DEC-009 + QG pass 3): added a minimal dbt-compatible jinja substitution to signalforge.warehouse.profiles so the demo's profiles.yml renders the operator's env var at load time. New typed error ProfileEnvVarUnsetError(ProfileNotFoundError).

Changes by surface

  • New code: src/signalforge/_demo/ (7 files, 22 KB), src/signalforge/demo/{__init__.py, errors.py}, src/signalforge/cli/init_demo.py, 4 new CliInitDemo*Error classes + 1 new ProfileEnvVarUnsetError, dbt-style env_var(...) rendering in signalforge.warehouse.profiles.
  • New tests: tests/test_demo.py (24 cases), tests/cli/test_init_demo.py (22 cases), tests/test_demo_fixture_parity.py (4), tests/test_wheel_packaging.py (2, gated), tests/cli/test_5_surface_parity_init_demo.py (6), plus subprocess init-demo --help smoke. Net +56 tests.
  • Doc updates: README.md (Quick Start swaps cp -r for init-demo), docs/cli-ops.md § Subcommands (new init-demo entry), CLAUDE.md (public API surface + cli: signalforge init-demo subcommand (Austin fixture for PyPI users) #47 repo-status entry). Rule files updated in US-009: python-build.md, cli-layer.md, testing-signal.md.

Testing

  • ruff check . && ruff format --check . && pyright && pytest → all clean (1735 passed, 1 skipped, 6 pre-existing WSL2 failures that pass in GitHub Actions Linux runners).
  • pytest -m wheel_smoke --no-cov → 2 passed (3.12s after pass-2-of-PR-review fixture refactor).
  • pytest -m cli_subprocess --no-cov → 2 passed.
  • E2E manual: signalforge init-demo /tmp/sf-demo && GOOGLE_CLOUD_PROJECT=foo /home/wesd/Projects/SignalForge/.venv/bin/python -c "from signalforge.warehouse.profiles import load_profile; print(load_profile(Path('/tmp/sf-demo')).project)"'foo'.

Compounding update

  • .claude/rules/python-build.md — new section on Hatch include directive for non-.py package data + wheel_smoke maintainer-gate pattern.
  • .claude/rules/cli-layer.md — extended subpackage layout for init_demo.py; new section "Library-surface pattern: CLI handler wraps a public lib module at the boundary" documenting the two-layer error wrapping pattern.
  • .claude/rules/testing-signal.mdwheel_smoke as the 5th gated marker.
  • plans/super/47-init-demo.md — appended "Quality-gate addendum" recording each of the 4 inline QG passes + the post-publish PR-review pass.

🤖 Generated with Claude Code

15 DECs locked across discovery / architecture / refinement; 7 implementation
stories + Quality Gate + Patterns & Memory. Architecture review surfaced two
load-bearing blockers (Hatch `include` directive required for non-`.py` data
files; `--force` semantics needed explicit choice) — both resolved before
detailing.

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

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements the signalforge init-demo subcommand (Issue #47) that copies a bundled Austin dbt demo project from the installed wheel into user destinations. It introduces a public signalforge.demo.copy_demo() library API with typed error hierarchy, wraps errors into CLI exit-code tiers, adds dbt profile env-var macro support, includes comprehensive end-to-end and surface-parity tests, and updates documentation and build/test rules.

Changes

Init-Demo Feature Implementation

Layer / File(s) Summary
Demo Library API and Error Hierarchy
src/signalforge/demo/errors.py, src/signalforge/demo/__init__.py, tests/test_demo.py
DemoError base class and four concrete subclasses (DemoPathError, DemoDestExistsError, DemoDestUnsafeError, DemoFixtureMissingError) with typed constructors and remediation-aware string rendering; public copy_demo(dest, *, force=False) function resolving paths, enforcing unsafe-destination guards, gating on destination existence, and atomically copying bundled _demo via importlib.resources.
Warehouse Profile Environment Variable Support
src/signalforge/warehouse/errors.py, src/signalforge/warehouse/__init__.py, src/signalforge/warehouse/profiles.py, tests/warehouse/test_errors.py, tests/warehouse/test_profiles.py
New ProfileEnvVarUnsetError exception for missing env_var() references in dbt profiles; env_var macro rendering in profiles.py via regex substitution with fallback support; warehouse package re-export; comprehensive test coverage for macro resolution, defaults, unset errors, and YAML quoting preservation.
CLI Error Wrappers and Exit-Code Registration
src/signalforge/cli/errors.py, src/signalforge/cli/_helpers.py, tests/cli/test_exit_codes.py
Four CLI-specific exception classes (CliInitDemo*) that wrap lower-level DemoError subclasses with command-specific messages and remediation strings; extended _EXCEPTION_TO_EXIT_CODE mapping registering ProfileEnvVarUnsetError and new demo/CLI errors across tiers; test factory support for parametrized exit-code validation.
CLI Subcommand Implementation and Registration
src/signalforge/cli/init_demo.py, src/signalforge/cli/__init__.py
Handler that calls copy_demo(), wraps Demo* exceptions into CliInitDemo* wrappers with consistent stderr formatting, prints next-steps message on success, and preserves KeyboardInterrupt/SystemExit semantics; argparse registration alongside version/lint/generate.
Demo Fixture Assets and Parity Enforcement
src/signalforge/_demo/*, tests/fixtures/dbt_project_austin/regenerate.sh, tests/test_demo_fixture_parity.py
Austin bikeshare dbt project packaged as wheel data (dbt_project.yml, profiles.yml with env_var macro, sources.yml, stg_bikeshare_trips.sql, signalforge.yml, manifest.json, .gitignore); fixture sync script mirroring fixture into _demo with demo-only rewrites; parity tests enforcing byte-for-byte equivalence except documented rewrites and zero-symlink policy.
Wheel Packaging Configuration and Smoke Testing
pyproject.toml, tests/test_wheel_packaging.py
Hatchling explicit include directive for src/signalforge/_demo/ for non-.py asset packaging; pytest wheel_smoke marker definition and exclusion from default addopts; maintainer-only tests verifying expected demo files and dotfile inclusion in built artifact.
CLI End-to-End and Smoke Testing
tests/cli/test_init_demo.py, tests/cli/test_5_surface_parity_init_demo.py, tests/cli/test_subprocess_smoke.py
In-process e2e tests covering fresh destination copy, empty/non-empty directory handling, unsafe force guards (home/cwd), destination shape validation, help output verification, and traceback-floor guarantee; 5-surface parity test ensuring init-demo/--force tokens consistent across argparse help, docstrings, docs/cli-ops.md, and plans/47; subprocess smoke test for init-demo --help output.
Audit, Scanning, and Documentation Updates
tests/test_audit_completeness.py, tests/llm/test_logger_grep_gate.py, .claude/rules/, CLAUDE.md, README.md, docs/cli-ops.md, plans/super/47-init-demo.md
Extended error-class taxonomy scan for DemoError abstract base and ten errors.py modules; demo subpackage logger scanning; warehouse test updates for ProfileEnvVarUnsetError; documentation reflecting eleven shipped issues, wheel-bundled demo workflow, init-demo CLI surface with exit-code mapping, comprehensive implementation plan with acceptance criteria and DECs; rule/marker updates for Python build and testing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • wjduenow/SignalForge#66: Both PRs extend CLI error classes and update the _EXCEPTION_TO_EXIT_CODE mapping in src/signalforge/cli/ files.
  • wjduenow/SignalForge#26: Both PRs extend CLI subcommand registration and exit-code mapping scaffolding, building on the same CLI-entrypoint infrastructure.

Poem

A rabbit hops through bundled streams,
Copy-demo sends, in YAML dreams,
Env vars bloom where profiles grew,
Wheels now roll with tests so true—
Init-demo brings the Austin view! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature being added—a new CLI subcommand 'signalforge init-demo' for distributing an Austin demo fixture to PyPI users—which aligns with the substantial changeset implementing this subcommand, library surface, documentation, and supporting infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@codecov-commenter

codecov-commenter commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

wjduenow and others added 21 commits May 11, 2026 16:42
Epic bd_1-scaffolding-t1o + 9 tasks (US-001..US-009) created with dependencies.
Plan phase advances to devolved; PR #78 stays in draft for review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the shipped Austin demo tree under src/signalforge/_demo/ as a copy
of tests/fixtures/dbt_project_austin/ with two named rewrites per DEC-008
of plans/super/47-init-demo.md:

- profiles.yml (DEC-009) — uses dbt's env_var('GOOGLE_CLOUD_PROJECT')
  macro so a PyPI user with that env var set runs the demo with zero
  file edits. The maintainer-only "DO NOT signalforge against this"
  header and the billing-broken project: bigquery-public-data
  placeholder (deliberately retained in the test-fixture copy so the
  issue-#10 e2e smoke test exercises the overwrite path) are stripped.
- .gitignore — slimmed to a single .signalforge/ exclusion; issue-#10
  / DEC-021 maintainer commentary is dropped (irrelevant to a demo
  user opening a fresh project).

The shipped tree is symlink-free (DEC-005) and excludes regenerate.sh
(maintainer-only).

New tests/test_demo_fixture_parity.py:

- test_demo_fixture_parity_holds_byte_for_byte_except_documented_files
  walks both trees, asserts every shared file is byte-equal except
  profiles.yml + .gitignore (which must DIFFER per the DECs above).
  Fails on uncommanded drift or a silent identical copy that would
  defeat DEC-009.
- test_demo_fixture_contains_no_symlinks codifies DEC-005.
- test_demo_profiles_yml_uses_env_var_macro pins DEC-009 specifically.
- test_test_fixture_profiles_yml_retains_maintainer_header confirms
  the rewrite is one-way and the e2e smoke fixture is untouched.

tests/fixtures/dbt_project_austin/regenerate.sh gains a final phase
(DEC-015) that mirrors every fixture file (except itself) into
src/signalforge/_demo/, then rewrites profiles.yml and .gitignore
verbatim from heredocs. Maintainers running the script keep both
trees aligned by construction; the parity test fires only on
uncommanded drift.

Validation:
- ruff check . clean
- ruff format --check . clean
- pyright clean (0 errors)
- pytest: 1658 passed, 4 new parity tests included; the 6 deselected
  symlink-loop tests are pre-existing env-specific failures on dev
  (Python 3.10 vs the project's tested >=3.11 floor) unrelated to
  this change.

Traces to: SD-1, DEC-005, DEC-008, DEC-009, DEC-015 of
plans/super/47-init-demo.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `signalforge.demo` public module exposing copy_demo(dest, *, force=False)
for library callers. Implements DEC-001 atomic-replace force semantics with
DEC-004 standalone path resolve + symlink-cycle guard. Surfaces DemoError
hierarchy (DemoPathError, DemoDestExistsError, DemoDestUnsafeError,
DemoFixtureMissingError) that the CLI layer (US-004) wraps into tier-mapped
Cli*Error subclasses at the handler boundary (DEC-012).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `[tool.hatch.build.targets.wheel] include = ["src/signalforge/_demo"]`
so the demo tree ships in the wheel (DEC-002). Registers wheel_smoke
pytest marker and adds tests/test_wheel_packaging.py which builds the
wheel via `python -m build --wheel` and asserts the canonical 8 demo files
appear in the artifact (DEC-003, including the .gitignore dotfile per
DEC-006).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `signalforge init-demo [<dest>]` subcommand at src/signalforge/cli/init_demo.py
mirroring the version.py / lint.py shape (DEC-009). Positional dest defaults to
./signalforge-demo/; --force triggers atomic-replace semantics through
signalforge.demo.copy_demo (DEC-001). Wraps the four DemoError subclasses at
the handler boundary into CliInitDemoDest{Exists,Unsafe}Error (tier 2) +
CliInitDemoFixtureMissingError + CliInitDemoCopyError (tier 1) — all four
registered in _EXCEPTION_TO_EXIT_CODE per the 7th AST scan (DEC-024). Emits
plain-text next-steps message to stdout naming GOOGLE_CLOUD_PROJECT and
ANTHROPIC_API_KEY env vars plus the three first-run commands (DEC-014).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends tests/cli/test_subprocess_smoke.py with a second
@pytest.mark.cli_subprocess test invoking `signalforge init-demo --help`
against the installed console script (DEC-010 / ticket AC-5). Catches
[project.scripts] regressions specifically against the new subcommand
that in-process main(argv) testing cannot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates README Quick Start to use `signalforge init-demo` instead of the
broken `cp -r tests/fixtures/...` snippet that assumed a clone (AC-4).
Strips the heredoc rewrite of profiles.yml since the shipped demo already
uses env_var('GOOGLE_CLOUD_PROJECT'). Adds the fourth subcommand entry to
docs/cli-ops.md § Subcommands following the lint precedent. Extends
CLAUDE.md "Public API surface" with the signalforge.demo.copy_demo entry
and the DemoError hierarchy (DEC-012); adds the #47 entry to the
repository-status block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/cli/test_5_surface_parity_init_demo.py mirroring the shape of
test_5_surface_parity_select.py (issue #37). Hard-asserts that the
subcommand name `init-demo` and the `--force` flag both appear in the four
external surfaces — argparse help, handler docstring, docs/cli-ops.md
§ Subcommands, and the plan's DEC list — so any future change to the
flag's contract has to land all four surfaces in the same commit
(cli-layer.md 5-surface parity rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on polish + test tightening

Quality-gate pass-1 review fixes:

- I-1 — refactor src/signalforge/demo.py → src/signalforge/demo/{__init__.py,
  errors.py} so the existing `*/errors.py` AST glob auto-covers the demo
  layer. Without the refactor, a v0.2 contributor adding a Demo*Error
  subclass would not get a test failure if they forgot to wire the CLI
  wrapper. Adds DemoError (abstract base) to _EXCEPTION_MAPPING_EXCLUDED_BASES
  and the four concretes (DemoPathError, DemoDestExistsError,
  DemoDestUnsafeError, DemoFixtureMissingError) to _EXCEPTION_TO_EXIT_CODE
  with same tiers as their CLI wrappers (defence-in-depth). Bumps the
  Scan-7 sanity test from 9 to 10 expected paths.
- I-2 — CliInitDemoDestExistsError default remediation now mentions the
  blast-radius guard ("refuses '/', $HOME, or the current working directory")
  so the operator who reads the remediation has full context before
  re-running with --force.
- I-4 — test_cmd_init_demo_never_leaks_traceback tightens its exit-code
  assertion from `ret in (1, 2)` to `assert ret == 1` so a regression
  routing the OSError to an input-validation wrapper surfaces loudly.
- N-1 — _NEXT_STEPS_MESSAGE shell-quotes the cd line via shlex.quote so
  destinations with spaces (e.g. "/Users/Wes Duenow/...") still produce
  a copy-pasteable shell command.

Validation: ruff / pyright clean; pytest 1722 passed; wheel_smoke 2/2;
audit_completeness 19/19 (including the now-extended Scan 7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass-2 review surfaced a UX bug: `copy_demo(dest=<existing_file>)` would
call `iterdir()` on a non-directory, raising raw `NotADirectoryError`
which the CLI wrapped as `CliInitDemoCopyError` (tier 1, "failed to
copy demo tree") — misleading because no copy was attempted.

Fix: add a shape-gate before the existence-gate in `copy_demo`. If
`dest` exists but is not a directory, raise `DemoDestExistsError` with
a clear "exists but is not a directory" message (still tier 2 via
`CliInitDemoDestExistsError`). Under `force=True`, the file is
unlinked and the demo tree copied in its place (semantically
consistent with `force` against a non-empty directory).

Adds two pinning tests:
- `test_copy_demo_dest_is_file_without_force_raises_dest_exists`
- `test_copy_demo_dest_is_file_with_force_replaces_with_demo_dir`
- `test_cmd_init_demo_dest_is_file_returns_exit_2_with_clear_message`

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

Pass-3 review surfaced a load-bearing blocker: the shipped demo's
profiles.yml uses `{{ env_var('GOOGLE_CLOUD_PROJECT') }}` but
signalforge.warehouse.profiles only does yaml.safe_load — no jinja
rendering — so the literal jinja string was sent to BigQuery as the
project ID, breaking the documented "zero file edits" happy path
promised in README, docs/cli-ops.md, and the next-steps message.

Fix:

- B-1 — Add minimal dbt-compatible env_var() substitution pass to
  signalforge.warehouse.profiles._load_profiles_yaml. Supports
  `env_var('NAME')` (raises ProfileEnvVarUnsetError when unset, no
  default) and `env_var('NAME', 'default')` (falls back to default).
  Pre-yaml-parse so YAML quoting is preserved.
  New typed error ProfileEnvVarUnsetError(ProfileNotFoundError) — tier
  1 (load-time / state-not-ready). Re-exported from
  signalforge.warehouse and registered in _EXCEPTION_TO_EXIT_CODE.
  Verified end-to-end: signalforge init-demo + load_profile resolves
  GOOGLE_CLOUD_PROJECT correctly; unset env surfaces a clear typed
  error pointing at the missing var instead of a BigQuery rejection.
  4 new tests in tests/warehouse/test_profiles.py pin the contract.

- I-1 — Add "demo" to _SCAN_SUBPACKAGES in
  tests/llm/test_logger_grep_gate.py so the ANSI-safe lazy-format
  rule auto-covers signalforge.demo. Pass-1's subpackage refactor
  promoted demo to a stage subpackage; the grep gate needed to keep
  pace.

- I-2 — Update .claude/rules/cli-layer.md "nine modules" / "nine
  per-stage abstract bases" prose to "ten" — issue #47 is the
  precedent for the count bump. Without this, a future maintainer
  reading the rule sees a stale count and either skips updating
  their own additions or trusts an out-of-date contract.

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

Pass-4 review surfaced one blocker:

- B-1 — ProfileEnvVarUnsetError (added pass 3) was registered in
  the CLI exit-code mapping and re-exported from signalforge.warehouse,
  but was missing from signalforge.warehouse.errors.__all__. The
  warehouse errors module's self-tests iterate __all__ to:
  - assert alphabetical sort + class count (one assertion in
    test_each_subclass_has_default_remediation pins the count)
  - smoke-construct each class via _CONSTRUCT_KWARGS
  - verify each carries a non-empty default_remediation
  All three skipped the new class, leaving a gap in the layer's
  "every typed exception is wired correctly" contract.

  Fix: add ProfileEnvVarUnsetError to __all__ in alphabetical position
  (between MaterialisationNotSupportedError and ProfileNotFoundError),
  bump the count constant from 19 to 20, and register the constructor
  kwargs (var_name= + profiles_path=) in _CONSTRUCT_KWARGS so the
  smoke loop exercises it.

- N-1 — Clean up the pass-3 cli-layer.md prose: the excluded-bases
  enumeration listed 11 names but said "ten" because LLMHelperError
  was incorrectly included (it's a concrete leaf raised directly in
  signalforge.llm.client, NOT an excluded abstract base). Removed
  LLMHelperError from the enumeration with an explicit "deliberately
  NOT excluded" note explaining why.

Validation: full pytest suite 1730 passed + 4 gated (2 wheel_smoke,
2 cli_subprocess), 0 regressions, ruff/pyright clean. Six
pre-existing WSL2 symlink-loop failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r + dual-surface library pattern + QG addendum

Captures patterns established by issue #47 into the rule files so future
maintainers don't re-discover them:

- .claude/rules/python-build.md — new section "Shipping package data
  (non-`.py` files) — explicit `include` directive". Documents the
  Hatch include directive pattern, the `wheel_smoke` maintainer-only
  marker shape (registered in pyproject, excluded by default addopts,
  built via python -m build / uvx fallback, inspected via
  zipfile.ZipFile), the belt-and-braces `unzip -l` verification step,
  and the dotfile-fragility caveat.

- .claude/rules/cli-layer.md — extended subpackage-layout listing to
  include init_demo.py as the fourth subcommand. Added a new section
  "Library-surface pattern: CLI handler wraps a public lib module at
  the boundary" documenting the two-layer error wrapping pattern
  (signalforge.demo's typed errors + signalforge.cli's tier-mapped
  wrappers). The 7th AST scan covers both layers; defence-in-depth
  via demo-layer errors also landing in _EXCEPTION_TO_EXIT_CODE.

- .claude/rules/testing-signal.md — extended gated-marker section
  with wheel_smoke as the 5th marker (alongside bigquery, anthropic,
  cli_subprocess, e2e). Documents the addopts exclusion list update
  and the --no-cov requirement for marker-specific runs.

- plans/super/47-init-demo.md — appended "Quality-gate addendum"
  section recording each of the 4 QG passes' refinements without
  rewriting the original 15 DECs in place. Preserves the ADR-style
  chronological design record.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow wjduenow changed the title 47: signalforge init-demo subcommand (plan) 47: signalforge init-demo subcommand (Austin fixture for PyPI users) May 12, 2026
@wjduenow
wjduenow requested a review from Copilot May 12, 2026 02:31
@wjduenow

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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

Codecov flagged patch coverage 93.21% with 11 lines missing across the new
files. Adds four targeted tests to close the gap:

- test_cmd_init_demo_demo_path_error_wraps_to_cli_path_error — exercises
  the DemoPathError → CliPathError handler branch via monkeypatch (the
  natural symlink-cycle test path skips on WSL2 where Path.resolve()
  doesn't raise on cycles).
- test_cmd_init_demo_keyboard_interrupt_propagates — pins the explicit
  re-raise in the (KeyboardInterrupt, SystemExit) catch (DEC-016
  carve-out so operator Ctrl-C reaches Python's default handler).
- test_cmd_init_demo_forward_compat_exception_belt_and_braces — pins
  the catch-all `except Exception` panic-path branch (tier 1; no
  traceback leak).
- test_cli_init_demo_error_constructors_render_without_cause — exercises
  the `cause is None` branch in all four CliInitDemo*Error constructors
  (existing exit-code-table tests always pass a cause).

Coverage: cli/init_demo.py 84% → 100%; the four new cli/errors.py
cause-None branches now covered. The two remaining file-level gaps
(cli/errors.py:120, demo/errors.py:61) are pre-existing on dev /
unreachable defensive paths and outside this PR's diff.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements issue #47 by shipping the Austin dbt demo project as wheel package-data and adding a new signalforge init-demo subcommand (plus a public signalforge.demo.copy_demo(...) entry point) so PyPI users can bootstrap a working first-run project without cloning the repo.

Changes:

  • Add signalforge init-demo [DEST] CLI subcommand + typed CLI errors + exit-code mapping + subprocess help smoke.
  • Introduce signalforge.demo.copy_demo(...) library API and ship src/signalforge/_demo/ (dbt project + locked manifest) in the wheel via Hatch include.
  • Add parity + packaging smoke tests, plus minimal dbt-style env_var(...) rendering for profiles.yml, and update docs/README/rules accordingly.

Reviewed changes

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

Show a summary per file
File Description
tests/warehouse/test_profiles.py Adds unit tests for dbt-style env_var(...) macro rendering in profiles.yml.
tests/warehouse/test_errors.py Updates warehouse error construction + __all__ count assertions to include ProfileEnvVarUnsetError.
tests/test_wheel_packaging.py Adds maintainer-only wheel_smoke tests that build the wheel and assert _demo/ files ship.
tests/test_demo.py Adds tests for signalforge.demo.copy_demo(...) behavior, force semantics, and typed errors.
tests/test_demo_fixture_parity.py Adds parity gate to keep shipped demo tree aligned with the e2e fixture (with documented rewrites).
tests/test_audit_completeness.py Extends AST-scan expectations for the new demo/errors.py module and excluded base DemoError.
tests/llm/test_logger_grep_gate.py Extends logger grep gate scan list to include the new demo subpackage.
tests/fixtures/dbt_project_austin/regenerate.sh Mirrors the fixture into src/signalforge/_demo/ and applies demo-specific rewrites.
tests/cli/test_subprocess_smoke.py Adds subprocess smoke for signalforge init-demo --help.
tests/cli/test_init_demo.py Adds in-process CLI tests for happy path, error tiers, help surface, and no-traceback floor.
tests/cli/test_exit_codes.py Extends synthetic constructor coverage for the new CLI wrappers and ProfileEnvVarUnsetError.
tests/cli/test_5_surface_parity_init_demo.py Adds 5-surface parity test ensuring init-demo and --force stay consistent across help/docs/plan.
src/signalforge/warehouse/profiles.py Adds minimal env_var(...) substitution pass before YAML parsing.
src/signalforge/warehouse/errors.py Adds ProfileEnvVarUnsetError and exports it via __all__.
src/signalforge/warehouse/init.py Re-exports ProfileEnvVarUnsetError from the warehouse package.
src/signalforge/demo/errors.py Introduces typed error hierarchy for the demo layer.
src/signalforge/demo/init.py Implements copy_demo(...) to copy packaged demo tree to disk with --force safety guardrails.
src/signalforge/cli/init_demo.py Implements the init-demo subcommand, next-steps message, and error wrapping.
src/signalforge/cli/errors.py Adds CliInitDemo*Error wrappers and default remediations.
src/signalforge/cli/_helpers.py Registers new errors (CLI + demo + warehouse) in _EXCEPTION_TO_EXIT_CODE.
src/signalforge/cli/init.py Registers the new init-demo subcommand with the top-level CLI parser.
src/signalforge/_demo/target/manifest.json Adds locked dbt manifest seed shipped in the demo tree.
src/signalforge/_demo/signalforge.yml Adds demo config shipped to users.
src/signalforge/_demo/profiles.yml Adds demo profiles.yml using {{ env_var('GOOGLE_CLOUD_PROJECT') }}.
src/signalforge/_demo/models/staging/stg_bikeshare_trips.sql Adds demo model SQL shipped to users.
src/signalforge/_demo/models/staging/sources.yml Adds demo sources YAML shipped to users.
src/signalforge/_demo/dbt_project.yml Adds demo dbt project definition shipped to users.
src/signalforge/_demo/.gitignore Adds slimmed demo .gitignore shipped to users.
README.md Updates Quick Start and CLI section to use signalforge init-demo instead of copying test fixtures.
pyproject.toml Adds Hatch wheel include, registers wheel_smoke marker, and excludes it from default pytest runs.
plans/super/47-init-demo.md Adds the full plan/DECs/stories document for issue #47.
docs/cli-ops.md Documents the new init-demo subcommand, semantics, and exit codes.
CLAUDE.md Updates repo status and public API surface to include signalforge.demo.copy_demo(...) and init-demo changes.
.claude/rules/testing-signal.md Documents the new wheel_smoke marker and --no-cov guidance.
.claude/rules/python-build.md Documents Hatch include for package data and the wheel smoke testing pattern.
.claude/rules/cli-layer.md Updates CLI layer rules/docs for new subcommand count and the “library surface + CLI wrapper” pattern.

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

Comment thread src/signalforge/warehouse/profiles.py Outdated
Comment thread tests/test_wheel_packaging.py Outdated
Comment thread plans/super/47-init-demo.md
@wjduenow
wjduenow marked this pull request as ready for review May 12, 2026 02:37
@wjduenow

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
tests/cli/test_init_demo.py (1)

42-51: ⚡ Quick win

Add a type annotation for _EXPECTED_TOP_LEVEL.

Please annotate this module-level constant explicitly as frozenset[str].

♻️ Suggested patch
-_EXPECTED_TOP_LEVEL = frozenset(
+_EXPECTED_TOP_LEVEL: frozenset[str] = frozenset(

As per coding guidelines, Use Python type hints throughout. All function signatures, class attributes, and module-level constants must be annotated.

🤖 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/cli/test_init_demo.py` around lines 42 - 51, Annotate the module-level
constant _EXPECTED_TOP_LEVEL with an explicit type by changing its declaration
to include a type hint of frozenset[str] (e.g., _EXPECTED_TOP_LEVEL:
frozenset[str] = ...); ensure you import typing generics if needed for your
Python version (or use built-in parametric frozenset if supported) and keep the
existing frozenset(...) initializer and values intact.
src/signalforge/demo/__init__.py (1)

43-50: ⚡ Quick win

Annotate __all__ with an explicit type.

__all__ should be declared as list[str] to conform to the repository typing rule for module-level constants.

♻️ Suggested patch
-__all__ = [
+__all__: list[str] = [

As per coding guidelines, Use Python type hints throughout. All function signatures, class attributes, and module-level constants must be annotated.

🤖 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 `@src/signalforge/demo/__init__.py` around lines 43 - 50, The module-level
constant __all__ is missing a type annotation; update its declaration to be
annotated as list[str] to satisfy repository typing rules (i.e., change the
__all__ definition to include the explicit type annotation list[str] while
keeping the same contents referencing "DemoDestExistsError",
"DemoDestUnsafeError", "DemoError", "DemoFixtureMissingError", "DemoPathError",
and "copy_demo"). Ensure you only add the type hint to the existing __all__
symbol and do not modify the exported names or order.
tests/test_demo_fixture_parity.py (1)

29-42: ⚡ Quick win

Add explicit type annotations to module-level constants.

_REPO_ROOT, _TEST_FIXTURE_DIR, _DEMO_DIR, _ALLOWED_REWRITES, and _TEST_FIXTURE_ONLY are unannotated. Please add explicit types to satisfy the project typing rule.

♻️ Suggested patch
-_REPO_ROOT = Path(__file__).resolve().parent.parent
-_TEST_FIXTURE_DIR = _REPO_ROOT / "tests" / "fixtures" / "dbt_project_austin"
-_DEMO_DIR = _REPO_ROOT / "src" / "signalforge" / "_demo"
+_REPO_ROOT: Path = Path(__file__).resolve().parent.parent
+_TEST_FIXTURE_DIR: Path = _REPO_ROOT / "tests" / "fixtures" / "dbt_project_austin"
+_DEMO_DIR: Path = _REPO_ROOT / "src" / "signalforge" / "_demo"
@@
-_ALLOWED_REWRITES = frozenset({"profiles.yml", ".gitignore"})
+_ALLOWED_REWRITES: frozenset[str] = frozenset({"profiles.yml", ".gitignore"})
@@
-_TEST_FIXTURE_ONLY = frozenset({"regenerate.sh"})
+_TEST_FIXTURE_ONLY: frozenset[str] = frozenset({"regenerate.sh"})

As per coding guidelines, Use Python type hints throughout. All function signatures, class attributes, and module-level constants must be annotated.

🤖 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/test_demo_fixture_parity.py` around lines 29 - 42, Add explicit type
annotations for the module-level constants: annotate _REPO_ROOT,
_TEST_FIXTURE_DIR, and _DEMO_DIR as pathlib.Path and annotate _ALLOWED_REWRITES
and _TEST_FIXTURE_ONLY as typing.FrozenSet[str]; import FrozenSet (or from
typing import FrozenSet) and pathlib.Path if not already imported, then change
the assignments to include the annotations (e.g., _REPO_ROOT: Path = ...,
_ALLOWED_REWRITES: FrozenSet[str] = frozenset({...})) while keeping the existing
values unchanged.
🤖 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/cli/init_demo.py`:
- Line 165: The call to copy_demo(...) uses raw args.dest and must instead be
validated via canonicalise_user_path; replace passing args.dest with a
canonicalised path obtained by calling canonicalise_user_path(args.dest,
project_dir) and handle any failure by raising or rewrapping the error as
CliPathError so no untrusted path reaches copy_demo; update the variable used by
copy_demo (resolved_dest) to the canonicalised result and ensure you reference
canonicalise_user_path and CliPathError in the fix.

In `@src/signalforge/demo/__init__.py`:
- Around line 90-138: The code currently calls
raw.expanduser().resolve(strict=False) (resolved_dest) which dereferences
symlinks and can make force=True delete the symlink target instead of the link;
fix by separating canonical checks from destructive operations: keep dest_path =
raw.expanduser() (no resolve) and use that for existence/shape/removal logic
(use dest_path.exists(), dest_path.is_dir(), dest_path.is_symlink()); only use
resolved_dest (or resolved_dest.resolve()) for comparing against unsafe_targets,
but when removing prefer: if dest_path.is_symlink(): dest_path.unlink() else:
shutil.rmtree(dest_path) so unlinking/rmtree operate on the path the user
supplied rather than a resolved target.

In `@src/signalforge/warehouse/errors.py`:
- Around line 162-170: The error message and remediation interpolate
profiles_path directly which can leak control characters; instead pass
profiles_path through the same safe formatter used for vars (e.g., call
_format_value(profiles_path)) where the message is constructed (the block that
builds the f-string using profiles_path and the remediation assignment) so both
the user-facing message and the remediation use the safely escaped
representation of profiles_path.

In `@src/signalforge/warehouse/profiles.py`:
- Around line 169-175: The comment above the env_var regex claims the outer {{
... }} are optional but the current pattern r"""\{\{\s*env_var\(...\)\s*\}\}"""
requires them; either make the outer braces optional in the regex (so it matches
both bare env_var('NAME') and {{ env_var('NAME') }}) or update the comment to
remove the “optional” claim; locate the regex literal in profiles.py (the
r"""\{\{\s*env_var... part) and implement the chosen change so the comment and
the implementation are consistent.

In `@tests/fixtures/dbt_project_austin/regenerate.sh`:
- Line 167: The cp call in the fallback mirror path (the line invoking cp
"${src_path}" "${dst_path}") is interactive and can prompt; change it to use a
non-interactive forced copy by adding the -f flag (i.e., use cp -f) so the copy
overwrites without prompting and conforms to the shell policy.

---

Nitpick comments:
In `@src/signalforge/demo/__init__.py`:
- Around line 43-50: The module-level constant __all__ is missing a type
annotation; update its declaration to be annotated as list[str] to satisfy
repository typing rules (i.e., change the __all__ definition to include the
explicit type annotation list[str] while keeping the same contents referencing
"DemoDestExistsError", "DemoDestUnsafeError", "DemoError",
"DemoFixtureMissingError", "DemoPathError", and "copy_demo"). Ensure you only
add the type hint to the existing __all__ symbol and do not modify the exported
names or order.

In `@tests/cli/test_init_demo.py`:
- Around line 42-51: Annotate the module-level constant _EXPECTED_TOP_LEVEL with
an explicit type by changing its declaration to include a type hint of
frozenset[str] (e.g., _EXPECTED_TOP_LEVEL: frozenset[str] = ...); ensure you
import typing generics if needed for your Python version (or use built-in
parametric frozenset if supported) and keep the existing frozenset(...)
initializer and values intact.

In `@tests/test_demo_fixture_parity.py`:
- Around line 29-42: Add explicit type annotations for the module-level
constants: annotate _REPO_ROOT, _TEST_FIXTURE_DIR, and _DEMO_DIR as pathlib.Path
and annotate _ALLOWED_REWRITES and _TEST_FIXTURE_ONLY as typing.FrozenSet[str];
import FrozenSet (or from typing import FrozenSet) and pathlib.Path if not
already imported, then change the assignments to include the annotations (e.g.,
_REPO_ROOT: Path = ..., _ALLOWED_REWRITES: FrozenSet[str] = frozenset({...}))
while keeping the existing values unchanged.
🪄 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: 73292f5a-2f54-46ef-88bc-c6668f7d6323

📥 Commits

Reviewing files that changed from the base of the PR and between 0103f88 and 6d4e616.

📒 Files selected for processing (36)
  • .claude/rules/cli-layer.md
  • .claude/rules/python-build.md
  • .claude/rules/testing-signal.md
  • CLAUDE.md
  • README.md
  • docs/cli-ops.md
  • plans/super/47-init-demo.md
  • pyproject.toml
  • src/signalforge/_demo/.gitignore
  • src/signalforge/_demo/dbt_project.yml
  • src/signalforge/_demo/models/staging/sources.yml
  • src/signalforge/_demo/models/staging/stg_bikeshare_trips.sql
  • src/signalforge/_demo/profiles.yml
  • src/signalforge/_demo/signalforge.yml
  • src/signalforge/_demo/target/manifest.json
  • src/signalforge/cli/__init__.py
  • src/signalforge/cli/_helpers.py
  • src/signalforge/cli/errors.py
  • src/signalforge/cli/init_demo.py
  • src/signalforge/demo/__init__.py
  • src/signalforge/demo/errors.py
  • src/signalforge/warehouse/__init__.py
  • src/signalforge/warehouse/errors.py
  • src/signalforge/warehouse/profiles.py
  • tests/cli/test_5_surface_parity_init_demo.py
  • tests/cli/test_exit_codes.py
  • tests/cli/test_init_demo.py
  • tests/cli/test_subprocess_smoke.py
  • tests/fixtures/dbt_project_austin/regenerate.sh
  • tests/llm/test_logger_grep_gate.py
  • tests/test_audit_completeness.py
  • tests/test_demo.py
  • tests/test_demo_fixture_parity.py
  • tests/test_wheel_packaging.py
  • tests/warehouse/test_errors.py
  • tests/warehouse/test_profiles.py

Comment thread src/signalforge/cli/init_demo.py
Comment thread src/signalforge/cli/init_demo.py
Comment thread src/signalforge/demo/__init__.py
Comment thread src/signalforge/warehouse/errors.py Outdated
Comment thread src/signalforge/warehouse/profiles.py Outdated
Comment thread tests/fixtures/dbt_project_austin/regenerate.sh Outdated
Fixed (6):

- [CRITICAL] CodeRabbit (demo/__init__.py:138): `--force` against a
  symlink dest would `rmtree()` the link's RESOLVED target rather than
  the link itself, potentially clobbering an unintended external
  location. Added pre-resolve symlink check that raises
  DemoDestUnsafeError before any destructive operation when
  `dest.is_symlink() and force=True`. No-force behaviour preserved
  (the existing test_copy_demo_with_symlink_dest_resolves_target
  contract — copy follows the link — still holds). Pinned by new
  test_copy_demo_force_against_symlink_dest_raises_dest_unsafe.

- CodeRabbit (warehouse/errors.py:170): ProfileEnvVarUnsetError
  interpolated `profiles_path` raw into message + remediation. Routed
  through `_format_value(str(profiles_path))` to match the rest of
  the warehouse error layer's safe-rendering convention (DEC-022 of
  warehouse-adapters.md).

- Copilot + CodeRabbit (profiles.py:172): `_ENV_VAR_RE` comment
  claimed bare `env_var('NAME')` form is accepted but the regex
  requires the outer `{{ }}` brackets. Updated the comment to match
  reality — the bracket-required form matches dbt's own jinja-render
  semantics; a bare form in YAML is just a string in dbt.

- Copilot (test_wheel_packaging.py:148): both wheel_smoke tests
  called `_build_wheel(tmp_path)` and rebuilt the wheel twice per
  run. Refactored to a module-scoped `_built_wheel_members` fixture
  that builds once and returns the artifact's namelist; both tests
  assert separate invariants over the shared member set. Run time
  drops from ~5-6s to ~3s.

- Copilot (regenerate.sh:167): fallback mirror used `cp` without
  `-f`. Added the flag per shell-non-interactive policy.

- Copilot (plans/super/47-init-demo.md:16): PR description said
  "Phase: detailing (awaiting approval)" but the plan now says
  "Phase: devolved" + QG addendum + all-9-stories-shipped. PR body
  updated in a follow-up REST PATCH (recorded in the resolve-summary
  comment).

False positives (2 — replied + resolved on the PR):

- CodeRabbit (init_demo.py:213): claimed init_demo needs a
  fail-closed audit JSONL write. Cites a fabricated guideline —
  init-demo is a copy operation, not a content-generating
  orchestrator that interacts with LLM/warehouse. Plan US-007
  explicitly says "no new audit-event class, no new fail-closed
  writer".
- CodeRabbit (init_demo.py:165): claimed dest must route through
  canonicalise_user_path. DEC-004 deliberately exempts init-demo
  (no project_dir at create-time; init-demo is the one entry point
  that *creates* a project, so the containment gate doesn't apply).
  Documented in the public copy_demo docstring.

Validation: ruff/pyright clean; pytest 1735 passed (was 1734);
wheel_smoke 2/2 pass in 3.12s.

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

Copy link
Copy Markdown
Owner Author

PR Review Summary

Round-2 review (Copilot + CodeRabbit) — addressed in commit 9a5f9df.

Fixed (6 items)

File Line Issue Reviewer Commit
src/signalforge/demo/__init__.py 138 CRITICAL--force against a symlink dest would rmtree() the link's resolved target, not the link itself, clobbering an arbitrary external location CodeRabbit 9a5f9df
src/signalforge/warehouse/errors.py 170 ProfileEnvVarUnsetError interpolated profiles_path raw into message + remediation; routed through _format_value() per warehouse-layer convention CodeRabbit 9a5f9df
src/signalforge/warehouse/profiles.py 172 _ENV_VAR_RE comment claimed bare env_var('NAME') form was accepted but regex requires {{ }}; updated comment to match reality (matches dbt jinja-render contract) Copilot + CodeRabbit 9a5f9df
tests/test_wheel_packaging.py 148 Both wheel_smoke tests rebuilt the wheel — extracted to module-scoped _built_wheel_members fixture, runtime down from ~5-6s to 3.12s Copilot 9a5f9df
tests/fixtures/dbt_project_austin/regenerate.sh 167 Fallback cp without -f → added flag per shell-non-interactive policy CodeRabbit 9a5f9df
plans/super/47-init-demo.md Meta + PR description PR description said "Phase: detailing (awaiting approval)" but plan now says "Phase: devolved" + QG complete; PR body rewritten via gh api -X PATCH Copilot (PR description edit)

False Positives (2 items)

File Line Issue Reason
src/signalforge/cli/init_demo.py 213 CodeRabbit: "init_demo is missing the fail-closed audit write path" citing a guideline that lists init_demo alongside draft_schema / prune_tests / etc. Fabricated guideline. init-demo is a one-shot copy operation — it doesn't consume warehouse data or call an LLM, so there is no PII / LLM-output to durably record. Plan plans/super/47-init-demo.md § US-007 explicitly says "no new audit-event class, no new fail-closed writer". The fail-closed pattern is for stages that interact with user data + external services (LLM, warehouse) where leak prevention is load-bearing.
src/signalforge/cli/init_demo.py 165 CodeRabbit: "dest must route through the CLI canonicalise_user_path gate" Documented deliberate exception. DEC-004 of the plan (and the copy_demo module docstring) state: "init-demo does not route dest through the project-wide canonicalise_user_path helper — that helper enforces a project_dir containment boundary appropriate for paths the CLI consumes inside an existing project. init-demo is the one entry point in the toolchain that creates a project, so the containment gate doesn't apply." Symlink-cycle defence still fires via Path.resolve()'s RuntimeError; the post-#9a5f9df symlink-dest guard covers the blast-radius case.

Validation post-fix

  • ruff check . && ruff format --check . && pyright && pytest → 1735 passed, 1 skipped, 6 pre-existing WSL2 failures (confirmed on dev HEAD, environmental).
  • pytest -m wheel_smoke --no-cov → 2 passed (3.12s).
  • pytest -m cli_subprocess --no-cov → 2 passed.

All 9 review threads resolved.

🤖 Generated with Claude Code

@wjduenow
wjduenow merged commit c977d7d into dev May 12, 2026
2 checks passed
@wjduenow
wjduenow deleted the feature/47-init-demo branch May 12, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants