Skip to content

#230: Airflow packaging + signalforge.airflow skeleton - #239

Merged
wjduenow merged 9 commits into
devfrom
feature/230-airflow-skeleton
Jun 15, 2026
Merged

#230: Airflow packaging + signalforge.airflow skeleton#239
wjduenow merged 9 commits into
devfrom
feature/230-airflow-skeleton

Conversation

@wjduenow

@wjduenow wjduenow commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Skeleton + packaging for the SignalForge Airflow integration (epic #228, v0.7). Stands up signalforge.airflow (one-shim seam, lazy __getattr__ re-exports, stub operators/hooks) + the [airflow] optional extra — zero Airflow weight in the base install. Builds on the #229 test-environment contract.

Status: implemented, in review. Full plan: plans/super/230-airflow-skeleton.md.

Key decisions

  • DEC-001: [airflow] extra is NOT mirrored into [dependency-groups].dev (documented deviation; default env stays Airflow-free).
  • DEC-004: stubs don't subclass BaseOperator at module scope (no-eager-import gate); real subclassing defers to children via the lazy shim factory.
  • DEC-008/009: confinement is a standalone AST+tokenize scan; the three gate tests run UNGATED.

Testing

  • Default suite green WITHOUT Airflow: pyright 0 errors, 3831 passed, wheel_smoke 7 passed; signalforge.airflow package 100% covered.
  • Gated leg re-certified locally against a constraints-pinned .venv-airflow (Airflow 2.10.4 / py3.11): .[airflow] resolves, lazy seam holds, DAG parses.

Compounding update

  • python-build.md: documents the deliberate not-mirrored-into-dev-group extra pattern.
  • cli-layer.md: scan-7 count → 14; airflow excluded-base posture.
  • CLAUDE.md: architecture-map row for signalforge.airflow.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an optional Apache Airflow integration available via the [airflow] extra (kept lightweight for base installs).
    • Introduced Airflow-related error types, including remediation text and CLI exit-code mapping (Airflow configuration errors return exit code 2).
  • Documentation

    • Expanded the architecture and Airflow skeleton/extra behavior documentation and contracts.
  • Tests

    • Added automated checks to ensure Airflow is not imported during lazy resolution, that the skeleton surface behaves safely, and that wheels exclude bundled Airflow while listing it only for the [airflow] extra.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edda4b3d-7cbd-4c27-9e81-2d906493aa2f

📥 Commits

Reviewing files that changed from the base of the PR and between 888e20c and 36f4e74.

📒 Files selected for processing (8)
  • .claude/rules/cli-layer.md
  • plans/super/230-airflow-skeleton.md
  • src/signalforge/airflow/_airflow_compat.py
  • src/signalforge/airflow/hooks.py
  • src/signalforge/airflow/operators.py
  • tests/airflow/test_airflow_import_confinement.py
  • tests/airflow/test_airflow_no_eager_import.py
  • tests/airflow/test_skeleton.py
✅ Files skipped from review due to trivial changes (2)
  • .claude/rules/cli-layer.md
  • plans/super/230-airflow-skeleton.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/airflow/test_skeleton.py
  • tests/airflow/test_airflow_no_eager_import.py
  • src/signalforge/airflow/operators.py
  • tests/airflow/test_airflow_import_confinement.py

📝 Walkthrough

Walkthrough

Introduces the signalforge.airflow subpackage as an Apache Airflow integration skeleton behind a new [airflow] optional extra. The package uses PEP 562 lazy exports and a single shim (_airflow_compat.py) to confine all real Airflow imports. A pure-Python error hierarchy is wired into CLI exit-code mapping. Three ungated gate tests enforce import confinement, no-eager-import behavior, and wheel packaging correctness.

Changes

signalforge.airflow Skeleton and Integration

Layer / File(s) Summary
Optional extra and CI install
pyproject.toml, .github/workflows/ci.yml, .claude/rules/python-build.md
Declares apache-airflow>=2.8,<3 behind the [airflow] optional extra (intentionally excluded from dependency-groups.dev), and flips the CI airflow job install from -e . to -e '.[airflow]'.
Airflow compat shim and protocols
src/signalforge/airflow/_airflow_compat.py
Adds the single designated seam for all real Airflow imports: two runtime_checkable protocols (_BaseOperatorProtocol, _BaseHookProtocol) and two lazy factories (make_base_operator, make_base_hook) that defer from airflow ... imports until called.
Package __init__ lazy exports and operator/hook stubs
src/signalforge/airflow/__init__.py, src/signalforge/airflow/operators.py, src/signalforge/airflow/hooks.py
Initializes signalforge.airflow with PEP 562 __getattr__/__dir__ for lazy operator and hook resolution. Adds SignalForgeGenerateOperator and SignalForgeHook skeleton classes whose constructors raise NotImplementedError.
Airflow error hierarchy and CLI exit-code wiring
src/signalforge/airflow/errors.py, src/signalforge/cli/_helpers.py
Adds AirflowIntegrationError (abstract base with remediation/default_remediation and custom __str__) and AirflowConfigError (concrete subclass). Wires AirflowConfigError into _EXCEPTION_TO_EXIT_CODE at tier 2 in the CLI helpers.
Scan-7 audit completeness updates
tests/test_audit_completeness.py, .claude/rules/cli-layer.md
Adds AirflowIntegrationError to the excluded abstract bases set, raises the expected errors.py discovery count from 13 to 14, and updates the cli-layer rule doc to match.
Import confinement gate test
tests/airflow/test_airflow_import_confinement.py
Enforces that all real airflow imports and airflow-mentioning type-ignore comments stay in the shim via AST+tokenize scanning. Tests verify confinement is actual, planted violations are caught, and false positives (package paths, docstrings) are excluded.
No-eager-import gate test
tests/airflow/test_airflow_no_eager_import.py
Verifies airflow never appears in sys.modules after importing signalforge.airflow or resolving its lazy names, via both in-process and subprocess tests. Runs un-gated to catch regressions even without Airflow installed.
Skeleton behavior tests
tests/airflow/test_skeleton.py
Tests shim import isolation, protocol runtime-checkability, stub NotImplementedError raises, package dir()/attr behavior, and _format_value repr-safety with control-character escaping.
Error hierarchy tests
tests/airflow/test_errors.py
Tests AirflowConfigError string rendering with remediation line, default remediation fallback, subclass assertion, and CLI exit-code 2 mapping. Avoids importing the real apache-airflow package.
Wheel packaging smoke tests
tests/test_wheel_packaging.py
Refactors wheel fixture to build once per module and share across tests. Adds tests verifying the wheel contains no vendored Airflow packages and that apache-airflow appears only as a gated [airflow] extra in Requires-Dist.
Super-plan and architecture documentation
plans/super/230-airflow-skeleton.md, CLAUDE.md
Adds the super-plan with design decisions DEC-001..DEC-009 and user stories US-001..US-006. Updates the CLAUDE.md architecture map with the signalforge.airflow skeleton entry.

Sequence Diagram(s)

sequenceDiagram
    participant Consumer
    participant "signalforge.airflow.__init__"
    participant "_airflow_compat"
    participant airflow

    Consumer->>signalforge.airflow.__init__: import AirflowConfigError
    Note over signalforge.airflow.__init__: Eagerly available — no airflow import triggered

    Consumer->>signalforge.airflow.__init__: access SignalForgeGenerateOperator
    signalforge.airflow.__init__->>operators: importlib.import_module(".operators")
    operators-->>signalforge.airflow.__init__: SignalForgeGenerateOperator (raises NotImplementedError on init)

    Consumer->>_airflow_compat: make_base_operator()
    _airflow_compat->>airflow: from airflow.models import BaseOperator
    airflow-->>_airflow_compat: BaseOperator class
    _airflow_compat-->>Consumer: returns BaseOperator

    Consumer->>signalforge.airflow.__init__: access unknown_attr
    signalforge.airflow.__init__-->>Consumer: raises AttributeError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • wjduenow/SignalForge#238: Adds the gated Airflow CI job and marker-driven execution that this PR modifies by flipping the install target to .[airflow].
  • wjduenow/SignalForge#162: Also updates the Scan-7/CLI-layer error-module counting rules and the _EXCEPTION_TO_EXIT_CODE mapping to accommodate newly added stage errors.py modules.
  • wjduenow/SignalForge#166: Both PRs extend the shared CLI-layer exception→exit-code mapping table and the Scan-7 audit exclusion bases for new error types.

Suggested labels

airflow

🐰 A skeleton of Airflow, light as a feather,
No eager imports to tangle together!
One shim holds the seam, protocols stand tall,
__getattr__ whispers when callers come to call.
The wheel ships clean, no Airflow in base —
Just a lazy-loaded, extra-gated place! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: introducing the signalforge.airflow skeleton subpackage and the [airflow] optional extra for packaging.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

wjduenow added 7 commits June 15, 2026 12:26
… + regenerate uv.lock

- regenerate uv.lock for the [airflow] extra (purely additive; no downgrades;
  default uv sync --dev stays airflow-free) — fixes stale-lock drift
- add tests/airflow/test_skeleton.py: base-env coverage of the shim protocols,
  both stubs raising NotImplementedError, __getattr__/__dir__, and _format_value
- mark make_base_operator/make_base_hook bodies '# pragma: no cover' (require
  the [airflow] extra; mirrors _snowflake_client.make_real_client)
- isolate the AST-import branch in the confinement planted-violation self-check
  (drop the type-ignore so it can't be caught by the comment branch)
- drop 'Async/' from make_base_hook docstring
…+ docs)

- python-build.md: document the [airflow]-extra deviation (heavy/constraints-only
  extra deliberately NOT mirrored into [dependency-groups].dev; backed by the
  no-eager-import gate + wheel-deps assertion)
- cli-layer.md: scan-7 count twelve -> fourteen; add SkillError +
  AirflowIntegrationError to the excluded-bases list; note airflow's excluded-only
  (no dual-registration) posture
- CLAUDE.md: add signalforge.airflow row to the architecture map (integration
  skeleton, v0.7 epic #228)
@wjduenow wjduenow changed the title #230: Airflow packaging + signalforge.airflow skeleton (plan) #230: Airflow packaging + signalforge.airflow skeleton Jun 15, 2026
@wjduenow
wjduenow marked this pull request as ready for review June 15, 2026 21:03
@wjduenow
wjduenow requested a review from Copilot June 15, 2026 21:05

@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

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

Inline comments:
In @.claude/rules/cli-layer.md:
- Around line 101-103: The abstract-base counts in this documentation are
inconsistent and need correction. The text states "ten of the twelve abstract
bases" but should reflect that there are 14 total abstract bases (as defined on
line 99), of which 10 have dual registration in _EXCEPTION_TO_EXIT_CODE and 4
are excluded-only exceptions (DemoError, IngestError, SkillError, and
AirflowIntegrationError). Update the opening statement of the Dual registration
section to change "twelve" to "fourteen" and adjust the supporting text to
accurately reflect that there are four deliberate exceptions rather than
implying only two, ensuring the policy contract language remains internally
consistent with the actual counts described in the paragraph.

In `@src/signalforge/airflow/_airflow_compat.py`:
- Around line 101-106: The __all__ list in the _airflow_compat.py module is
currently exporting underscore-prefixed protocol names (_BaseHookProtocol and
_BaseOperatorProtocol), which violates the convention that underscore-prefixed
symbols are internal and should not be part of the public API surface. Remove
the two underscore-prefixed protocol names from the __all__ list, keeping only
the factory functions make_base_hook and make_base_operator as the public
exports. The protocols will remain directly importable for internal and test
use, but will no longer be part of the official public contract.

In `@src/signalforge/airflow/hooks.py`:
- Line 32: The `__init__` method signature contains unused variadic parameters
that should be marked as intentionally unused according to the repo's coding
guidelines. Rename the `*args` parameter to `*_args` and the `**kwargs`
parameter to `**_kwargs` in the `__init__` method to prefix them with an
underscore, indicating they are internal placeholder parameters that are not
part of the public API and are intentionally unused.

In `@src/signalforge/airflow/operators.py`:
- Line 33: The `__init__` constructor method declares variadic parameters
`*args` and `**kwargs` that are not used within the method body. According to
the coding guidelines, unused or internal-only parameters should be prefixed
with an underscore to signal they are intentional placeholders. Change the
parameter names from `*args, **kwargs` to `*_args, **_kwargs` in the method
signature to mark these as internal implementation details.

In `@tests/airflow/test_airflow_import_confinement.py`:
- Around line 75-83: The false-positive occurs because the code checks if
"airflow" is present anywhere in the full line text, including in import paths
like signalforge.airflow. To fix this, modify the airflow detection to only
check the code portion of the line (before the comment starts), not the comment
itself. Use the comment token's start column position (tok.start[1]) to slice
the line and check only the code portion: examine line_text[:tok.start[1]]
instead of the full line_text when checking if "airflow" is present. This
ensures that "airflow" appearing in a package name or import path is not
mistakenly flagged as a violation.

In `@tests/airflow/test_airflow_no_eager_import.py`:
- Around line 79-84: The test cleanup loop only removes airflow and airflow.*
modules from sys.modules, but prior imports of signalforge.airflow* modules can
remain cached and cause the subsequent import statement to skip the lazy-name
resolution path you intend to test. Extend the cleanup condition in the loop
that checks for name == "airflow" or name.startswith("airflow.") to also check
for name == "signalforge.airflow" or name.startswith("signalforge.airflow.") so
that all previously cached signalforge.airflow modules are also deleted before
the test import runs.
🪄 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: 0f376254-52a2-4ff3-b628-c749cbac6fd5

📥 Commits

Reviewing files that changed from the base of the PR and between 258f731 and 888e20c.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .claude/rules/cli-layer.md
  • .claude/rules/python-build.md
  • .github/workflows/ci.yml
  • CLAUDE.md
  • plans/super/230-airflow-skeleton.md
  • pyproject.toml
  • src/signalforge/airflow/__init__.py
  • src/signalforge/airflow/_airflow_compat.py
  • src/signalforge/airflow/errors.py
  • src/signalforge/airflow/hooks.py
  • src/signalforge/airflow/operators.py
  • src/signalforge/cli/_helpers.py
  • tests/airflow/test_airflow_import_confinement.py
  • tests/airflow/test_airflow_no_eager_import.py
  • tests/airflow/test_errors.py
  • tests/airflow/test_skeleton.py
  • tests/test_audit_completeness.py
  • tests/test_wheel_packaging.py

Comment thread .claude/rules/cli-layer.md Outdated
Comment thread src/signalforge/airflow/_airflow_compat.py
Comment thread src/signalforge/airflow/hooks.py Outdated
Comment thread src/signalforge/airflow/operators.py Outdated
Comment thread tests/airflow/test_airflow_import_confinement.py
Comment thread tests/airflow/test_airflow_no_eager_import.py

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 introduces the initial signalforge.airflow integration skeleton and an [airflow] optional extra that keeps Apache Airflow completely out of the base install/import path, backed by ungated tests that enforce “core stays lean” invariants.

Changes:

  • Add signalforge.airflow skeleton package (lazy public re-exports, one-shim Airflow import seam, stub operator/hook, typed errors).
  • Add ungated guardrail tests (no-eager-import, import/type-ignore confinement) plus wheel-level assertions that apache-airflow is not vendored and not a core dependency.
  • Wire packaging/CI/docs/scans to recognize the new stage (airflow/errors.py) and install .[airflow] in the gated workflow job.

Reviewed changes

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

Show a summary per file
File Description
pyproject.toml Adds [project.optional-dependencies].airflow with apache-airflow>=2.8,<3.
src/signalforge/airflow/__init__.py Establishes lazy public surface via PEP 562 __getattr__ while eagerly re-exporting Airflow-free errors.
src/signalforge/airflow/_airflow_compat.py Single confined shim for all from airflow ... imports (lazy factories + Protocols).
src/signalforge/airflow/errors.py Adds AirflowIntegrationError + AirflowConfigError remediation-rendering seam.
src/signalforge/airflow/operators.py Adds Airflow-free stub operator raising NotImplementedError.
src/signalforge/airflow/hooks.py Adds Airflow-free stub hook raising NotImplementedError.
src/signalforge/cli/_helpers.py Registers AirflowConfigError in _EXCEPTION_TO_EXIT_CODE (tier 2).
tests/test_audit_completeness.py Updates Scan 7 exclusions + expected errors.py modules list to include airflow/errors.py.
tests/test_wheel_packaging.py Adds wheel METADATA/core-deps negative assertions for apache-airflow; refactors wheel build fixture.
tests/airflow/test_airflow_import_confinement.py Enforces all Airflow imports/type-ignores confined to _airflow_compat.py (with planted-violation checks).
tests/airflow/test_airflow_no_eager_import.py Ungated in-process + subprocess checks that importing signalforge.airflow never imports airflow.
tests/airflow/test_skeleton.py Default-suite tests for the Airflow-free skeleton surface (protocols/stubs/error helpers).
tests/airflow/test_errors.py Pins remediation rendering and exit-code mapping for AirflowConfigError.
.github/workflows/ci.yml Updates gated airflow job to install -e '.[airflow]' under constraints.
CLAUDE.md Adds signalforge.airflow to the architecture map summary table.
plans/super/230-airflow-skeleton.md Adds the plan document detailing decisions and deliverables for #230.
.claude/rules/python-build.md Documents the deliberate exception: [airflow] extra is not mirrored into the dev dependency group.
.claude/rules/cli-layer.md Updates Scan 7 documentation for new bases/modules (but contains a count mismatch noted in comments).

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

Comment thread .claude/rules/cli-layer.md Outdated
Comment thread tests/airflow/test_skeleton.py Outdated
Comment thread plans/super/230-airflow-skeleton.md
- _airflow_compat __all__: drop underscore-prefixed protocols (internal, not public API)
- operators/hooks stubs: *_args/**_kwargs for intentionally-unused variadics
- confinement test: comment-branch gates on the comment token, not the full line
  (no false-positive on 'from signalforge.airflow ... # type: ignore')
- no-eager + skeleton tests: scrub signalforge.airflow* / fresh-import so the
  no-import assertions are test-order-independent
- cli-layer.md: ten-of-fourteen + four excluded-only bases (count consistency)
- plan Meta: phase -> complete (in review)
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary

All review comments addressed in 36f4e74. No false positives — every thread was a real fix.

Fixed (9 threads)

File Issue Resolution
_airflow_compat.py Underscore-prefixed protocols in __all__ Dropped — only the factory functions are public; protocols stay importable internals
operators.py, hooks.py Unused variadic params *_args, **_kwargs per the _-prefix convention
test_airflow_import_confinement.py Comment-branch false-positive on from signalforge.airflow … # type: ignore Gate on the comment token, not the full line
test_airflow_no_eager_import.py Lazy-resolution test scrubbed only airflow* Also scrub signalforge.airflow* so the import re-runs the lazy path
test_skeleton.py No-import assertion was order-dependent Scrub + fresh import_module (order-independent)
cli-layer.md (×2) "ten of the twelve" inconsistent with the 14-base list "ten of the fourteen"; four excluded-only bases named
plan / PR body Phase mismatch Plan Meta + PR body reconciled to "implemented, in review"

Validation

pyright 0 errors · 3831 passed · wheel_smoke 7 passed · signalforge.airflow 100% covered · gated airflow leg re-certified against .venv-airflow (Airflow 2.10.4).

@wjduenow wjduenow added the airflow Apache Airflow orchestration integration label Jun 15, 2026
@wjduenow
wjduenow merged commit e91dff7 into dev Jun 15, 2026
11 of 19 checks passed
@wjduenow
wjduenow deleted the feature/230-airflow-skeleton branch June 15, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

airflow Apache Airflow orchestration integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants