Skip to content

#235: Airflow drift / signal-rot detection (run-over-run) - #245

Merged
wjduenow merged 20 commits into
devfrom
feature/235-drift-detection
Jun 16, 2026
Merged

#235: Airflow drift / signal-rot detection (run-over-run)#245
wjduenow merged 20 commits into
devfrom
feature/235-drift-detection

Conversation

@wjduenow

@wjduenow wjduenow commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Closes #235. Part of epic #228 (v0.7 Airflow); builds on #231 (result/XCom contract) and #232 (SignalForgeGenerateOperator).

What

Run-over-run drift detection: compare a SignalForge run (N) against its prior run (N-1) and emit a structured drift report — the headline scheduled value-add that turns a SignalForge DAG into a schema-drift / signal-rot monitor.

  • Signal rot — a test that was kept and is now dropped: always-passes (the alarm).
  • Grade regression — mean grade fell ≥ threshold (default 0.05, tunable).
  • Tier transitions (newly_dropped / newly_kept), schema-shape (column add/remove), all reported; only signal-rot + grade-regression page.

Shape

  • Airflow-free pure core signalforge.airflow.driftcompute_drift(...) -> DriftReport + fail-soft loaders; 100% ungated coverage; reusable by the v0.8 GitHub Action.
  • Two surfaces: a detect_drift_against flag on SignalForgeGenerateOperator (run + compare in one task) and a dedicated SignalForgeDriftOperator (reads two sidecars downstream, branchable).
  • on_drift (fail/skip/succeed) is the run-over-run analogue of on_flagged — combined via most-severe-wins; byte-identical when no drift is passed (no TaskOutcome/exit-tier changes, no new error class).
  • Templated-path history (detect_drift_against + drift_history_dir with {{ ds }} / {{ prev_ds }}); persistence reuses the existing fail-closed write_sidecar (no new writer, no new audit class).
  • Degrade-never-fail on baseline / model-mismatch / corrupt-prior / --no-grade. Deterministic (sorted iteration + two blake2b-8 input hashes), reproducible at (model, as_of).

Tests & certification

  • Pure core / loaders / decide_task_outcome extension / deferred-operator skeleton — ungated; StrictDriftReport schema-stability drift detector + committed fixture.
  • Gated @pytest.mark.airflow execute() tests (both surfaces) + example DAG (signalforge_drift_monitor_dag.py) + DagBag-parse.
  • Full suite: ruff + format + pyright (0 errors) + 4082 passed. Certified vs Airflow 2.10.4 (50 gated tests).
  • Quality Gate: 4 diverse-angle reviews; fixed a schema-shape artifact_id arity bug (+ regression test) and two docs-accuracy items.

Docs

docs/airflow-ops.md — new "Drift / signal-rot detection" section (ticket A8). Working conventions captured in .claude/rules/airflow-integration.md.

Plan: plans/super/235-drift-detection.md (19 DECs). Devolved + built via beads epic (8 stories).

Summary by CodeRabbit

New Features

  • Drift and signal-rot detection: Added run-over-run comparison to detect tier changes, grade regressions, and schema shape deltas.
  • Airflow integration updates: Introduced a dedicated drift operator and extended the generate operator with drift persistence and policy control (on_drift).

Documentation

  • Expanded Airflow ops docs with drift semantics, XCom payload expectations, and failure/alerting behavior.
  • Added the full drift detection super-plan and updated supporting references.
  • Included an example DAG demonstrating both integration patterns.

Tests & Examples

  • Added comprehensive unit tests for drift core, sidecar loaders, operator behavior, DAG parsing, and schema stability, plus new drift fixtures.

wjduenow added 18 commits June 16, 2026 12:22
@wjduenow
wjduenow requested a review from Copilot June 16, 2026 21:25
@wjduenow
wjduenow marked this pull request as ready for review June 16, 2026 21:25
@coderabbitai

coderabbitai Bot commented Jun 16, 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: 7dd87492-db68-44d7-b19c-d16e290e0f9a

📥 Commits

Reviewing files that changed from the base of the PR and between 1f85caf and 4345f04.

📒 Files selected for processing (5)
  • .claude/rules/airflow-integration.md
  • docs/airflow-ops.md
  • examples/airflow/signalforge_drift_monitor_dag.py
  • src/signalforge/airflow/operators.py
  • tests/airflow/test_operators_helpers.py
✅ Files skipped from review due to trivial changes (1)
  • .claude/rules/airflow-integration.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/airflow-ops.md
  • examples/airflow/signalforge_drift_monitor_dag.py
  • tests/airflow/test_operators_helpers.py
  • src/signalforge/airflow/operators.py

📝 Walkthrough

Walkthrough

Adds run-over-run drift/signal-rot detection to SignalForge Airflow integration via a pure Pydantic drift core (compute_drift, loaders), extended SignalForgeGenerateOperator, new SignalForgeDriftOperator, drift-aware task outcomes, example DAG, fixtures, and comprehensive test coverage.

Changes

Drift / Signal-Rot Detection Feature

Layer / File(s) Summary
Drift core: Pydantic models, compute_drift function, and fail-soft loaders
src/signalforge/airflow/drift.py
Defines DriftArtifact, GradeRegression, SchemaShapeDelta, DriftReport Pydantic models with alarming property and to_xcom() serialization; implements deterministic compute_drift comparison engine with hashing, artifact-id column extraction, and transition classification; adds fail-soft load_diff_report, load_grade_report, and parse_diff_report with symlink-loop hardening and size-limit enforcement.
Result module: drift-aware task outcome decision
src/signalforge/airflow/result.py
Adds OnDrift literal type; introduces severity ordering constants and policy-to-outcome helpers; extends decide_task_outcome to accept on_drift and drift parameters and return the most-severe of flagged vs drift outcomes for exit-code 0 while preserving non-zero exit-tier behavior.
Operators module: generate extension and SignalForgeDriftOperator factory
src/signalforge/airflow/operators.py
Extends SignalForgeGenerateOperator with four drift parameters (detect_drift_against, drift_history_dir, on_drift, grade_regression_threshold), template fields, and _execute_single drift/persistence path; implements public build_drift_report, internal helpers (_validate_drift_config, _build_drift_inputs, _drift_task_outcome), _make_drift_operator_class factory for new SignalForgeDriftOperator, and _DriftOperatorAirflowMissing placeholder.
Package exports: drift core re-export and lazy drift operator
src/signalforge/airflow/__init__.py
Eagerly re-exports drift core symbols (compute_drift, DriftArtifact, DriftReport, GradeRegression, SchemaShapeDelta); adds SignalForgeDriftOperator to lazy name registry and __all__.
Example DAG: signalforge_drift_monitor with two patterns
examples/airflow/signalforge_drift_monitor_dag.py
Adds _config env/Variable/default resolver, date-templated history path computation, and two drift-monitoring operator arrangements: ergonomic single-task SignalForgeGenerateOperator with on_drift="fail", and two-task generate >> drift_check pipeline using SignalForgeDriftOperator as gate.
Committed test fixtures: drift pair sidecars and schema fixture
tests/fixtures/airflow/drift_pairs/*, tests/fixtures/airflow/drift_report_v1.json
Adds JSON fixture files for drift pairs (prev/curr diff and grade sidecars) and canonical drift_report_v1.json schema fixture used by schema stability and integration tests.
Ungated tests: drift core compute_drift, loaders, and schema stability
tests/airflow/test_drift_core.py, tests/airflow/test_drift_loaders.py, tests/airflow/test_drift_report_schema.py
Covers transition classification (signal-rot, newly_dropped, newly_kept), schema-shape derivation, grade regression detection, degrade/mismatch behavior, determinism, alarming truth table, to_xcom serialization/JSON round-tripping, why truncation, fail-soft loader behavior, symlink-loop hardening, and strict Pydantic mirror field-set parity/extra-mode enforcement.
Ungated tests: operator helpers and result drift-axis decision
tests/airflow/test_operators_helpers.py, tests/airflow/test_result.py
Validates on_drift parameter acceptance, build_drift_report orchestration, _validate_drift_config, _build_drift_inputs, and _drift_task_outcome mapping; extends decide_task_outcome tests with comprehensive drift/flagged outcome combination matrix across exit codes and policies.
Gated Airflow tests: drift operator end-to-end and DAG parsing
tests/airflow/test_drift_operators.py, tests/airflow/test_dag_parse.py
Tests SignalForgeGenerateOperator drift path (alarm handling, baseline, degrade, unparseable current, history persistence) and SignalForgeDriftOperator path (alarm handling, baseline, error handling, construction validation); verifies drift_monitor_dag parsing and drift operator template field rendering.
Skeleton and import tests: lazy access and construction guard
tests/airflow/test_airflow_no_eager_import.py, tests/airflow/test_skeleton.py
Extends no-eager-import tests to verify SignalForgeDriftOperator lazy resolution without importing airflow; adds skeleton test asserting drift operator import is airflow-free and construction raises when airflow is unavailable.
Documentation, plan, and integration rules
plans/super/235-drift-detection.md, docs/airflow-ops.md, .claude/rules/airflow-integration.md
Adds epic plan with architecture decisions and eight-story breakdown; expands Airflow ops docs with drift detection section, two operator surfaces, on_drift outcome mapping, and test coverage; updates integration rules with drift specification and reference links.

Sequence Diagram(s)

sequenceDiagram
    participant DAG as Airflow DAG
    participant GenOp as SignalForgeGenerateOperator
    participant DriftOp as SignalForgeDriftOperator
    participant Core as compute_drift (drift.py)
    participant FS as Filesystem (diff.json sidecars)
    participant Outcome as decide_task_outcome

    rect rgba(100, 149, 237, 0.5)
        Note over DAG,Outcome: Ergonomic single-operator flow
        DAG->>GenOp: execute(context)
        GenOp->>FS: run signalforge CLI, parse stdout → current diff
        GenOp->>FS: load_diff_report(detect_drift_against) → prior diff
        GenOp->>Core: compute_drift(prior, current, grades)
        Core-->>GenOp: DriftReport
        GenOp->>FS: persist diff.json/grade.json to drift_history_dir
        GenOp->>Outcome: decide_task_outcome(result, on_drift, drift)
        Outcome-->>DAG: SUCCESS / AirflowSkipException / AirflowFailException
    end

    rect rgba(144, 238, 144, 0.5)
        Note over DAG,Outcome: Two-task generate + dedicated drift gate
        DAG->>GenOp: execute(context), on_drift="succeed"
        GenOp->>FS: persist diff.json sidecar
        GenOp-->>DAG: XCom with drift nested under "drift" key
        DAG->>DriftOp: execute(context)
        DriftOp->>FS: load_diff_report(previous_diff_path)
        DriftOp->>FS: load_diff_report(current_diff_path)
        DriftOp->>Core: compute_drift(prior, current)
        Core-->>DriftOp: DriftReport
        DriftOp->>Outcome: _drift_task_outcome(report, on_drift)
        Outcome-->>DAG: SUCCESS / AirflowSkipException / AirflowFailException
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • wjduenow/SignalForge#239: Introduced the lazy-operator mapping and no-eager-import skeleton in signalforge.airflow.__init__ that this PR extends with SignalForgeDriftOperator.
  • wjduenow/SignalForge#240: Modified decide_task_outcome in src/signalforge/airflow/result.py, the same function this PR extends with the on_drift/drift axis.
  • wjduenow/SignalForge#241: Implemented SignalForgeGenerateOperator in src/signalforge/airflow/operators.py, which this PR extends with drift parameters, template fields, and _execute_single drift path.

Suggested labels

airflow

Poem

🐇 Hop hop, the signals drift away,
A compute_drift checks the diff each day.
Signal rot caught, the alarm rings true,
on_drift="fail" — the pipeline knew!
XCom carries the tale of tier and grade,
No secrets spilled, no bulk text displayed.
The rabbit approves: drift found, not afraid! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.38% 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 '#235: Airflow drift / signal-rot detection (run-over-run)' clearly and concisely summarizes the main feature being implemented: drift/signal-rot detection for Airflow DAGs with run-over-run comparison.
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds run-over-run drift detection to the signalforge.airflow integration so scheduled DAG runs can compare run N vs N-1 and emit a structured drift report (signal-rot + grade-regression paging, plus informational tier/schema-shape deltas). This extends the existing Airflow result→task-state contract to incorporate an optional drift verdict while keeping the drift core Airflow-free and reusable.

Changes:

  • Introduces an Airflow-free drift core (compute_drift -> DriftReport) with fail-soft sidecar/stdout loaders and XCom-safe serialization.
  • Adds two Airflow surfaces: drift-on-generate (detect_drift_against / drift_history_dir / on_drift) and a dedicated SignalForgeDriftOperator.
  • Adds extensive ungated + gated tests, committed fixtures, an example DAG, and docs/rules updates.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/signalforge/airflow/drift.py New Airflow-free drift core (DriftReport, compute_drift) + fail-soft loaders.
src/signalforge/airflow/operators.py Wires drift into SignalForgeGenerateOperator and adds deferred SignalForgeDriftOperator + pure helpers.
src/signalforge/airflow/result.py Extends decide_task_outcome with optional drift policy (on_drift) and most-severe-wins combine.
src/signalforge/airflow/init.py Eagerly re-exports drift core; adds lazy export for the new drift operator.
tests/airflow/test_drift_core.py Ungated unit tests for drift classification, determinism, alarming semantics, and to_xcom.
tests/airflow/test_drift_loaders.py Ungated tests pinning fail-soft loader behavior (absent/corrupt/oversize/symlink-loop).
tests/airflow/test_drift_report_schema.py Ungated strict-schema drift detector + fixture validation for DriftReport.
tests/airflow/test_drift_operators.py Gated Airflow execute() tests for both drift surfaces and outcome mapping.
tests/airflow/test_operators_helpers.py Ungated tests for new pure operator helpers (build_drift_report, drift validation/path helpers, outcomes).
tests/airflow/test_result.py Extends result/outcome tests to cover drift policy interactions.
tests/airflow/test_skeleton.py Ungated skeleton test ensuring drift operator name is resolvable without importing Airflow.
tests/airflow/test_airflow_no_eager_import.py Extends no-eager-import test to include the drift operator.
tests/airflow/test_dag_parse.py Updates DagBag parse coverage to include the new example drift-monitor DAG and templating.
tests/fixtures/airflow/drift_report_v1.json Committed fixture for DriftReport schema-stability detector.
tests/fixtures/airflow/drift_pairs/signal_rot_prev_diff.json Committed engineered “prev” diff fixture for signal-rot/grade-regression comparisons.
tests/fixtures/airflow/drift_pairs/signal_rot_curr_diff.json Committed engineered “curr” diff fixture for signal-rot/grade-regression comparisons.
tests/fixtures/airflow/drift_pairs/signal_rot_prev_grade.json Committed engineered “prev” grade fixture for regression comparisons.
tests/fixtures/airflow/drift_pairs/signal_rot_curr_grade.json Committed engineered “curr” grade fixture for regression comparisons.
examples/airflow/signalforge_drift_monitor_dag.py New example DAG demonstrating both drift surfaces (ergonomic + branchable).
docs/airflow-ops.md Adds operator-facing documentation for drift detection, policies, and example usage.
.claude/rules/airflow-integration.md Captures integration patterns/decisions for drift detection.
plans/super/235-drift-detection.md Detailed implementation plan / DEC log for #235.

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

Comment thread src/signalforge/airflow/operators.py Outdated
Comment thread src/signalforge/airflow/operators.py
Comment thread docs/airflow-ops.md Outdated
Comment thread examples/airflow/signalforge_drift_monitor_dag.py Outdated
Comment thread .claude/rules/airflow-integration.md Outdated
…ction

# Conflicts:
#	.claude/rules/airflow-integration.md
#	docs/airflow-ops.md
#	src/signalforge/airflow/operators.py
#	tests/airflow/test_dag_parse.py
#	tests/airflow/test_operators_helpers.py
#	tests/airflow/test_skeleton.py

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/signalforge/airflow/operators.py (1)

264-343: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate grade_regression_threshold before computing drift.

grade_regression_threshold is accepted from both operator surfaces but never validated. Invalid values (negative/NaN/inf) can silently misclassify regressions and produce incorrect alarming behavior.

Proposed fix
@@
+import math
@@
+def _validate_grade_regression_threshold(value: float) -> None:
+    if isinstance(value, bool) or not isinstance(value, (int, float)):
+        raise AirflowConfigError(
+            "`grade_regression_threshold` must be a finite number >= 0."
+        )
+    numeric = float(value)
+    if not math.isfinite(numeric) or numeric < 0:
+        raise AirflowConfigError(
+            f"`grade_regression_threshold` must be a finite number >= 0 (got {value!r})."
+        )
@@
 def _validate_operator_config(
@@
-    drift_history_dir: str | None = None,
+    drift_history_dir: str | None = None,
+    grade_regression_threshold: float = 0.05,
 ) -> None:
@@
+    _validate_grade_regression_threshold(grade_regression_threshold)
@@
 def _validate_drift_config(
@@
-    on_drift: str,
+    on_drift: str,
+    grade_regression_threshold: float,
 ) -> None:
@@
+    _validate_grade_regression_threshold(grade_regression_threshold)

Also applies to: 627-669, 877-880, 1524-1527

🤖 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/airflow/operators.py` around lines 264 - 343, The parameter
`grade_regression_threshold` is accepted from operator surfaces but lacks
validation, allowing invalid values like negative numbers, NaN, or infinity to
silently cause incorrect regression classification and alarming behavior. Add
`grade_regression_threshold` as a parameter to the `_validate_operator_config`
function signature, then add validation logic that ensures the value is either
None/unset or is a valid positive number (check that it is a numeric type, is
not negative, and is not NaN or infinity). Apply the same validation pattern at
all other locations where this parameter is accepted from operator
configurations, mirroring the validation approach used for other numeric or
optional parameters in the function.
🤖 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 `@examples/airflow/signalforge_drift_monitor_dag.py`:
- Around line 178-195: The `SignalForgeGenerateOperator` task with
`task_id="generate"` currently only sets `on_drift="succeed"` but does not
explicitly set `on_flagged`, which means it inherits the default
`on_flagged="fail"`. In this branchable pattern where generate should never fail
and instead let the downstream dedicated operator handle the gating, add
`on_flagged="succeed"` to the operator configuration to ensure flagged runs also
succeed rather than blocking downstream execution of the `drift_check` task.

In `@src/signalforge/airflow/operators.py`:
- Around line 334-335: The whitespace-only string handling is inconsistent
across multiple locations in the file. The validation logic in
_validate_operator_config (lines 334-335) correctly treats whitespace-only
strings as empty by stripping them, but the runtime checks that use
self.detect_drift_against and self.drift_history_dir at lines 902-904,
1024-1025, and 1144-1145 treat whitespace-only strings as truthy/enabled. Apply
the same whitespace-stripping check at each of these runtime locations: update
the conditions to check not just if the attribute exists, but also verify it is
not None and not just whitespace (using the same pattern as the validation:
isinstance check and .strip() call). This ensures whitespace-only paths are
consistently treated as disabled everywhere.

---

Outside diff comments:
In `@src/signalforge/airflow/operators.py`:
- Around line 264-343: The parameter `grade_regression_threshold` is accepted
from operator surfaces but lacks validation, allowing invalid values like
negative numbers, NaN, or infinity to silently cause incorrect regression
classification and alarming behavior. Add `grade_regression_threshold` as a
parameter to the `_validate_operator_config` function signature, then add
validation logic that ensures the value is either None/unset or is a valid
positive number (check that it is a numeric type, is not negative, and is not
NaN or infinity). Apply the same validation pattern at all other locations where
this parameter is accepted from operator configurations, mirroring the
validation approach used for other numeric or optional parameters in the
function.
🪄 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: a523b04b-4603-4e02-86d3-10c74d1cad09

📥 Commits

Reviewing files that changed from the base of the PR and between 9276335 and 1f85caf.

📒 Files selected for processing (22)
  • .claude/rules/airflow-integration.md
  • docs/airflow-ops.md
  • examples/airflow/signalforge_drift_monitor_dag.py
  • plans/super/235-drift-detection.md
  • src/signalforge/airflow/__init__.py
  • src/signalforge/airflow/drift.py
  • src/signalforge/airflow/operators.py
  • src/signalforge/airflow/result.py
  • tests/airflow/test_airflow_no_eager_import.py
  • tests/airflow/test_dag_parse.py
  • tests/airflow/test_drift_core.py
  • tests/airflow/test_drift_loaders.py
  • tests/airflow/test_drift_operators.py
  • tests/airflow/test_drift_report_schema.py
  • tests/airflow/test_operators_helpers.py
  • tests/airflow/test_result.py
  • tests/airflow/test_skeleton.py
  • tests/fixtures/airflow/drift_pairs/signal_rot_curr_diff.json
  • tests/fixtures/airflow/drift_pairs/signal_rot_curr_grade.json
  • tests/fixtures/airflow/drift_pairs/signal_rot_prev_diff.json
  • tests/fixtures/airflow/drift_pairs/signal_rot_prev_grade.json
  • tests/fixtures/airflow/drift_report_v1.json

Comment thread examples/airflow/signalforge_drift_monitor_dag.py
Comment thread src/signalforge/airflow/operators.py
…rmalization, degrade docs, branchable example on_flagged)
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary

All 7 review threads addressed — 7 fixed, 0 false positives. Pushed in 4345f04.

Fixed (7 items)

File Issue Fix
operators.py (generate __init__) grade_regression_threshold accepted without validation — a string/negative value would crash or mis-behave in compute_drift Added _validate_grade_regression_threshold (rejects non-numeric incl. bool, and < 0) → fail fast with AirflowConfigError
operators.py (drift operator __init__) same, on the dedicated operator same validation call added
operators.py (generate __init__ + execute) whitespace-only drift paths validated as "off" but read as "on" at runtime Added _blank_str_to_none; normalize detect_drift_against/drift_history_dir at construction AND at execute entry (covers rendered template_fields)
docs/airflow-ops.md claimed corrupt/unreadable prior → degrade_reason Corrected: fail-soft load_diff_report returns None for absent and malformed alike → both collapse to a baseline; degrade_reason is set only on a model_unique_id mismatch
.claude/rules/airflow-integration.md same overstatement (DEC-013 bullet) same correction
examples/.../signalforge_drift_monitor_dag.py (docstring) same overstatement same correction
examples/.../signalforge_drift_monitor_dag.py (branchable generate task) default on_flagged="fail" could fail the upstream generate and prevent drift_check from running Added on_flagged="succeed" (record-only generate; the dedicated operator is the single pageable gate) + clarified comment

Tests

6 new ungated tests in tests/airflow/test_operators_helpers.py pin _blank_str_to_none (whitespace→None, real-value/template passthrough) and _validate_grade_regression_threshold (accepts valid numbers; rejects non-numeric, bool, negative).

Validation

ruff ✓ · ruff format ✓ · pyright 0 errors · 4119 passed (default suite) · 75 gated airflow tests pass vs Airflow 2.10.4.

@wjduenow
wjduenow merged commit 678da32 into dev Jun 16, 2026
7 checks passed
@wjduenow
wjduenow deleted the feature/235-drift-detection branch June 16, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants