From ba09a51f0fc3943e035753516f1a644b57209d18 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Thu, 1 Oct 2026 13:03:38 -0700 Subject: [PATCH 1/3] feat(airflow): add Airflow skills with an executable eval suite and benchmark Add three Apache Airflow skills, chosen from measured agent failures: - `authoring-airflow-dags`: version-aware DAG writing and fixing with a verify-until-clean loop, period contracts for date semantics, deferrable sensors / retry budgets and DAG-factory references - `migrating-to-airflow-3`: 2.x -> 3.x ports that preserve behaviour, including runtime-only traps ruff misses and a BEFORE/AFTER replay - `testing-airflow-dags`: offline pytest suites with a mutation check Shared stdlib-only scripts (`airflow_check.py`, `manual_run.py`, `replay_runs.py`) live in `skills/airflow/_shared/` and are vendored byte-identically; tests enforce the copies match. Eval suite and harness: - `evals/airflow/cases/`: 20 core + 6 hard cases with fixtures, reference and alternative solutions, mutants, and graders that execute the agent's code in real Airflow 3.3.2 / 2.11.2; self-test 239 variants, 0 violations - `evals/harness/`: baseline vs skill A/B runner for Claude Code and altimate-code with per-run isolation (fresh config dir, macOS sandbox incl. signal isolation, per-attempt TMPDIR, separate agent venv, inventory check), failure accounting, resume, sanitized artifacts - trigger eval (30 queries incl. near-misses) Benchmark (`evals/airflow/BENCHMARK.md`, Claude Code, Sonnet 5.5 / Opus 5.5): hard suite holdout 4/6 -> 6/6 (Sonnet), 5/6 -> 6/6 (Opus); core suite is near ceiling without skills. Earlier altimate-code runs on Sonnet 4.6 / Haiku 4.5 are included as a secondary result. Also: `airflow-skills` marketplace plugin, README sections, CONTRIBUTING skill-structure and eval guidance (drops the non-spec `triggers:` key), CI workflow (spec lint, Airflow tests, grader self-test), `.gitattributes` keeping eval fixtures byte-exact. Co-Authored-By: Claude Opus 5.5 (1M context) --- .claude-plugin/marketplace.json | 11 + .gitattributes | 3 + .github/workflows/ci.yml | 97 + .gitignore | 21 + CONTRIBUTING.md | 111 +- README.md | 58 +- evals/airflow/BENCHMARK.md | 320 + evals/airflow/cases/_example_smoke/case.yaml | 19 + .../fixture/dags/orders_daily.py | 32 + .../_example_smoke/fixture/data/events.csv | 11 + .../_example_smoke/fixture/requirements.txt | 1 + evals/airflow/cases/_example_smoke/grade.py | 78 + .../airflow2_imports/dags/event_counts.py | 36 + .../reference/dags/event_counts.py | 41 + .../alt_reference/dags/inventory_snapshot.py | 55 + .../case.yaml | 38 + .../fixture/.gitignore | 2 + .../fixture/README.md | 10 + .../fixture/dags/sales_daily.py | 53 + .../fixture/data/inventory_movements.csv | 427 + .../fixture/data/reorder_points.csv | 9 + .../fixture/data/sales.csv | 6 + .../fixture/requirements.txt | 6 + .../grade.py | 125 + .../grader_data/inventory_movements.csv | 432 + .../dags/inventory_snapshot.py | 65 + .../dags/inventory_snapshot.py | 64 + .../dags/inventory_snapshot.py | 65 + .../dags/inventory_snapshot.py | 65 + .../dags/inventory_snapshot.py | 65 + .../reference/dags/inventory_snapshot.py | 65 + .../dags/clickstream_page_stats.py | 66 + .../case.yaml | 36 + .../fixture/.gitignore | 2 + .../fixture/README.md | 12 + .../fixture/dags/marketing_spend_daily.py | 43 + .../data/clickstream/2026-09-27.csv.gz | Bin 0 -> 297531 bytes .../data/clickstream/2026-09-28.csv.gz | Bin 0 -> 317640 bytes .../data/clickstream/2026-09-29.csv.gz | Bin 0 -> 337383 bytes .../fixture/data/marketing_spend.csv | 7 + .../fixture/requirements.txt | 4 + .../grade.py | 157 + .../dags/clickstream_page_stats.py | 79 + .../dags/clickstream_page_stats.py | 77 + .../dags/clickstream_page_stats.py | 52 + .../dags/clickstream_page_stats.py | 60 + .../reference/dags/clickstream_page_stats.py | 76 + .../alt_reference/dags/daily_revenue.py | 39 + .../case.yaml | 34 + .../fixture/.gitignore | 2 + .../fixture/README.md | 11 + .../fixture/dags/store_dimension.py | 31 + .../fixture/data/orders.csv | 330 + .../fixture/data/stores.csv | 5 + .../fixture/requirements.txt | 4 + .../grade.py | 159 + .../cron_string_uses_ds/dags/daily_revenue.py | 49 + .../dags/daily_revenue.py | 51 + .../dags/daily_revenue.py | 49 + .../dags/daily_revenue.py | 46 + .../reference/dags/daily_revenue.py | 49 + .../alt_reference/dags/partner_feeds.py | 57 + .../authoring-partner-feeds-mapping/case.yaml | 41 + .../fixture/.gitignore | 2 + .../fixture/README.md | 11 + .../fixture/dags/partner_registry_export.py | 32 + .../data/partner_drop/2026-09-02/acme.csv | 11 + .../data/partner_drop/2026-09-03/acme.csv | 26 + .../data/partner_drop/2026-09-03/globex.csv | 15 + .../data/partner_drop/2026-09-03/initech.csv | 4 + .../data/partner_drop/2026-09-03/umbrella.csv | 9 + .../data/partner_drop/2026-09-04/globex.csv | 12 + .../data/partner_drop/2026-09-04/hooli.csv | 5 + .../fixture/data/partners.json | 7 + .../fixture/requirements.txt | 3 + .../authoring-partner-feeds-mapping/grade.py | 174 + .../partner_drop/2026-09-02/globex.csv | 7 + .../partner_drop/2026-09-03/acme.csv | 7 + .../partner_drop/2026-09-03/hooli.csv | 8 + .../partner_drop/2026-09-03/initech.csv | 9 + .../partner_drop/2026-09-03/wayne.csv | 10 + .../dags/partner_feeds.py | 65 + .../dags/partner_feeds.py | 69 + .../dags/partner_feeds.py | 44 + .../dags/partner_feeds.py | 66 + .../dags/partner_feeds.py | 65 + .../reference/dags/partner_feeds.py | 65 + .../alt_reference/dags/vendor_shipments.py | 57 + .../authoring-vendor-shipments-etl/case.yaml | 48 + .../fixture/.gitignore | 4 + .../fixture/README.md | 16 + .../fixture/dags/warehouse_orders.py | 53 + .../data/incoming/shipments_2026-09-28.csv | 26 + .../fixture/data/orders_export.csv | 7 + .../fixture/requirements.txt | 4 + .../fixture/scripts/build_shipments_report.py | 36 + .../scripts/publish_shipments_report.sh | 10 + .../fixture/scripts/refresh_orders_mart.sh | 15 + .../authoring-vendor-shipments-etl/grade.py | 284 + .../landing/shipments_2026-09-28.csv | 32 + .../landing/shipments_2026-09-29.csv | 43 + .../dags/vendor_shipments.py | 69 + .../append_only_load/dags/vendor_shipments.py | 68 + .../dags/vendor_shipments.py | 69 + .../dags/vendor_shipments.py | 67 + .../dags/vendor_shipments.py | 69 + .../dags/vendor_shipments.py | 70 + .../reference/dags/vendor_shipments.py | 69 + .../alt_reference/dags/partner_feed.py | 81 + .../case.yaml | 47 + .../fixture/dags/partner_catalog_sync.py | 33 + .../fixture/dags/partner_feed.py | 74 + .../fixture/data/partner_orders.csv | 43 + .../fixture/requirements.txt | 1 + .../debugging-manual-trigger-context/grade.py | 178 + .../dags/partner_feed.py | 85 + .../mutants/local_tz_day/dags/partner_feed.py | 87 + .../mutants/now_fallback/dags/partner_feed.py | 86 + .../run_after_always/dags/partner_feed.py | 86 + .../skip_manual_runs/dags/partner_feed.py | 88 + .../start_date_fallback/dags/partner_feed.py | 86 + .../template_left/dags/partner_feed.py | 86 + .../reference/dags/partner_feed.py | 86 + .../alt_reference/dags/warehouse_ingest.py | 58 + .../case.yaml | 56 + .../fixture/README.md | 11 + .../fixture/config/tables.json | 10 + .../fixture/dags/warehouse_ingest.py | 83 + .../fixture/dags/warehouse_maintenance.py | 32 + .../fixture/data/raw/customers.csv | 4 + .../fixture/data/raw/inventory.csv | 4 + .../fixture/data/raw/order_items.csv | 8 + .../fixture/data/raw/orders.csv | 6 + .../fixture/data/raw/payments.csv | 10 + .../fixture/data/raw/products.csv | 9 + .../fixture/data/raw/refunds.csv | 5 + .../fixture/data/raw/shipments.csv | 7 + .../fixture/requirements.txt | 2 + .../debugging-nondeterministic-parse/grade.py | 331 + .../catalog_at_parse/dags/warehouse_ingest.py | 78 + .../description_date/dags/warehouse_ingest.py | 62 + .../hardcoded_tables/dags/warehouse_ingest.py | 62 + .../now_start_date/dags/warehouse_ingest.py | 62 + .../parallel_loads/dags/warehouse_ingest.py | 62 + .../set_order/dags/warehouse_ingest.py | 62 + .../dags/warehouse_ingest.py | 63 + .../reference/dags/warehouse_ingest.py | 62 + .../alt_reference/dags/warehouse_load.py | 78 + .../debugging-variable-at-parse/case.yaml | 48 + .../fixture/README.md | 16 + .../fixture/dags/order_status_report.py | 37 + .../fixture/dags/warehouse_load.py | 76 + .../fixture/data/orders.csv | 59 + .../fixture/requirements.txt | 2 + .../debugging-variable-at-parse/grade.py | 130 + .../catchup_enabled/dags/warehouse_load.py | 83 + .../daily_preset/dags/warehouse_load.py | 83 + .../default_at_parse/dags/warehouse_load.py | 81 + .../hardcoded_path/dags/warehouse_load.py | 83 + .../schedule_dropped/dags/warehouse_load.py | 82 + .../dags/warehouse_load.py | 77 + .../reference/dags/warehouse_load.py | 83 + .../alt_reference/dags/lake/sensors.py | 68 + .../alt_reference/dags/lake/triggers.py | 63 + .../case.yaml | 72 + .../fixture/.gitignore | 2 + .../fixture/README.md | 25 + .../fixture/dags/.airflowignore | 1 + .../fixture/dags/lake/__init__.py | 0 .../fixture/dags/lake/hooks.py | 54 + .../fixture/dags/lake/sensors.py | 66 + .../fixture/dags/lake/triggers.py | 66 + .../fixture/dags/lake_ingest.py | 35 + .../fixture/dags/orders_diff.py | 34 + .../fixture/requirements.txt | 4 + .../fixture/tools/catalog_stub.py | 104 + .../grade.py | 212 + .../grader_data/defer_runner.py | 290 + .../eval_dags/_eval_lake_scenarios.py | 77 + .../grader_data/lake_api.py | 104 + .../dags/lake/triggers.py | 65 + .../blocking_hook_kept/dags/lake/hooks.py | 54 + .../dags/lake/sensors.py | 79 + .../dags/lake/triggers.py | 70 + .../errors_as_success/dags/lake/sensors.py | 81 + .../dags/lake/triggers.py | 63 + .../dags/lake/triggers.py | 68 + .../dags/lake/sensors.py | 82 + .../dags/lake/triggers.py | 66 + .../timeout_retryable/dags/lake/sensors.py | 83 + .../reference/dags/lake/hooks.py | 62 + .../reference/dags/lake/sensors.py | 83 + .../reference/dags/lake/triggers.py | 63 + .../alt_reference/dags/partners/sensors.py | 85 + .../case.yaml | 53 + .../fixture/.gitignore | 2 + .../fixture/README.md | 23 + .../fixture/dags/.airflowignore | 1 + .../fixture/dags/partner_ingest.py | 41 + .../fixture/dags/partners/__init__.py | 0 .../fixture/dags/partners/hooks.py | 45 + .../fixture/dags/partners/sensors.py | 40 + .../fixture/requirements.txt | 4 + .../fixture/tools/manifest_stub.py | 94 + .../grade.py | 205 + .../grader_data/defer_runner.py | 290 + .../eval_dags/_eval_manifest_scenarios.py | 42 + .../grader_data/manifest_api.py | 94 + .../dags/partners/sensors.py | 83 + .../anchor_in_xcom/dags/partners/sensors.py | 87 + .../anchor_this_try/dags/partners/sensors.py | 83 + .../dags/partners/sensors.py | 87 + .../dags/partners/sensors.py | 43 + .../dags/partners/triggers.py | 52 + .../dags/partners/sensors.py | 86 + .../dags/partners/triggers.py | 48 + .../dags/partners/triggers.py | 47 + .../dags/partners/sensors.py | 89 + .../reference/dags/partners/hooks.py | 58 + .../reference/dags/partners/sensors.py | 87 + .../reference/dags/partners/triggers.py | 47 + .../dags/claims_intake_hourly.py | 54 + .../alt_reference/dags/claims_reprocess.py | 71 + .../dags/claims_service_date_marts.py | 80 + .../dags/denials_digest_daily.py | 53 + .../dags/provider_directory_daily.py | 60 + .../plugins/claims_lib/assets.py | 23 + .../alt_reference/plugins/claims_lib/hooks.py | 29 + .../alt_reference/requirements.txt | 3 + .../case.yaml | 32 + .../fixture/.gitignore | 3 + .../fixture/README.md | 30 + .../fixture/dags/claims_intake_hourly.py | 54 + .../fixture/dags/claims_reprocess.py | 66 + .../fixture/dags/claims_service_date_marts.py | 76 + .../fixture/dags/denials_digest_daily.py | 50 + .../fixture/dags/provider_directory_daily.py | 53 + .../fixture/data/claims_export.csv | 65 + .../fixture/data/providers_export.csv | 29 + .../fixture/plugins/claims_lib/__init__.py | 7 + .../fixture/plugins/claims_lib/assets.py | 23 + .../fixture/plugins/claims_lib/hooks.py | 29 + .../fixture/requirements.txt | 2 + .../hard-migration-claims-partitions/grade.py | 65 + .../hardsim.py | 658 + .../dags/denials_digest_daily.py | 50 + .../dags/denials_digest_daily.py | 53 + .../dags/provider_directory_daily.py | 53 + .../dags/claims_intake_hourly.py | 54 + .../dags/claims_service_date_marts.py | 79 + .../dags/claims_service_date_marts.py | 81 + .../dags/claims_reprocess.py | 67 + .../dags/claims_reprocess.py | 67 + .../reference/dags/claims_intake_hourly.py | 54 + .../reference/dags/claims_reprocess.py | 67 + .../dags/claims_service_date_marts.py | 78 + .../reference/dags/denials_digest_daily.py | 52 + .../dags/provider_directory_daily.py | 55 + .../reference/plugins/claims_lib/assets.py | 23 + .../reference/plugins/claims_lib/hooks.py | 29 + .../reference/requirements.txt | 3 + .../alt_reference/dags/close_report_weekly.py | 67 + .../alt_reference/dags/daily_close.py | 74 + .../alt_reference/dags/fx_revaluation.py | 28 + .../alt_reference/dags/gl_postings_hourly.py | 53 + .../alt_reference/dags/ledger_housekeeping.py | 29 + .../alt_reference/dags/sql/daily_close.sql | 13 + .../alt_reference/dags/sql/fx_revaluation.sql | 24 + .../alt_reference/dags/vendor_payments.py | 47 + .../plugins/ledger_lib/operators.py | 49 + .../alt_reference/requirements.txt | 4 + .../hard-migration-ledger-close/case.yaml | 33 + .../fixture/.gitignore | 3 + .../fixture/README.md | 32 + .../fixture/dags/close_report_weekly.py | 66 + .../fixture/dags/daily_close.py | 64 + .../fixture/dags/fx_revaluation.py | 27 + .../fixture/dags/gl_postings_hourly.py | 48 + .../fixture/dags/ledger_housekeeping.py | 29 + .../fixture/dags/sql/daily_close.sql | 12 + .../fixture/dags/sql/fx_revaluation.sql | 24 + .../fixture/dags/vendor_payments.py | 47 + .../fixture/data/fx_rates.csv | 31 + .../fixture/data/postings.csv | 120 + .../fixture/plugins/ledger_lib/__init__.py | 7 + .../fixture/plugins/ledger_lib/operators.py | 49 + .../fixture/plugins/ledger_lib/workdays.py | 54 + .../fixture/plugins/ledger_plugin.py | 11 + .../fixture/requirements.txt | 3 + .../hard-migration-ledger-close/grade.py | 66 + .../hard-migration-ledger-close/hardsim.py | 658 + .../plugins/ledger_lib/operators.py | 49 + .../dags/daily_close.py | 74 + .../close_trigger_skipped/dags/daily_close.py | 75 + .../dags/sql/daily_close.sql | 14 + .../dags/gl_postings_hourly.py | 51 + .../dags/gl_postings_hourly.py | 49 + .../dags/ledger_housekeeping.py | 29 + .../payments_utc_cron/dags/vendor_payments.py | 47 + .../dags/close_report_weekly.py | 67 + .../reference/dags/close_report_weekly.py | 67 + .../reference/dags/daily_close.py | 74 + .../reference/dags/fx_revaluation.py | 28 + .../reference/dags/gl_postings_hourly.py | 51 + .../reference/dags/ledger_housekeeping.py | 29 + .../reference/dags/sql/daily_close.sql | 13 + .../reference/dags/sql/fx_revaluation.sql | 24 + .../reference/dags/vendor_payments.py | 47 + .../reference/plugins/ledger_lib/operators.py | 49 + .../reference/requirements.txt | 4 + .../alt_reference/_DELETE | 1 + .../alt_reference/dags/billing_daily.py | 86 + .../alt_reference/dags/meter_registry.py | 35 + .../alt_reference/dags/regional_readings.py | 33 + .../dags/scripts/publish_invoices.sh | 8 + .../alt_reference/dags/tariffs_daily.py | 38 + .../alt_reference/dags/usage_rollup.py | 84 + .../alt_reference/plugins/meter_lib/assets.py | 14 + .../plugins/meter_lib/operators.py | 56 + .../alt_reference/requirements.txt | 3 + .../hard-migration-meter-rollups/case.yaml | 33 + .../fixture/.gitignore | 3 + .../fixture/README.md | 28 + .../fixture/config/regions.json | 1 + .../fixture/dags/billing_daily.py | 73 + .../fixture/dags/meter_registry.py | 36 + .../fixture/dags/regional_readings.py | 33 + .../fixture/dags/scripts/publish_invoices.sh | 8 + .../fixture/dags/scripts/sync_tariffs.sh | 9 + .../fixture/dags/tariffs_daily.py | 27 + .../fixture/dags/usage_rollup.py | 67 + .../fixture/data/readings_east.csv | 865 + .../fixture/data/readings_west.csv | 577 + .../fixture/data/tariffs.csv | 13 + .../fixture/plugins/meter_lib/__init__.py | 1 + .../fixture/plugins/meter_lib/assets.py | 14 + .../fixture/plugins/meter_lib/operators.py | 56 + .../fixture/plugins/meter_lib/tariffs.py | 15 + .../fixture/plugins/meter_plugin.py | 10 + .../fixture/requirements.txt | 2 + .../hard-migration-meter-rollups/grade.py | 76 + .../hard-migration-meter-rollups/hardsim.py | 658 + .../dags/billing_daily.py | 83 + .../dags/usage_rollup.py | 87 + .../plugins/meter_lib/operators.py | 56 + .../dags/billing_daily.py | 83 + .../dags/usage_rollup.py | 87 + .../dags/usage_rollup.py | 86 + .../tariffs_bare_cron/dags/tariffs_daily.py | 27 + .../dags/scripts/sync_tariffs.sh | 12 + .../dags/scripts/sync_tariffs.sh | 12 + .../reference/dags/billing_daily.py | 84 + .../reference/dags/meter_registry.py | 35 + .../reference/dags/regional_readings.py | 33 + .../dags/scripts/publish_invoices.sh | 8 + .../reference/dags/scripts/sync_tariffs.sh | 12 + .../reference/dags/tariffs_daily.py | 28 + .../reference/dags/usage_rollup.py | 84 + .../reference/plugins/meter_lib/assets.py | 14 + .../reference/plugins/meter_lib/operators.py | 56 + .../reference/requirements.txt | 3 + .../alt_reference/dags/pipeline_factory.py | 142 + .../alt_reference/pools.json | 4 + .../hard-reliability-factory-fleet/case.yaml | 104 + .../fixture/README.md | 20 + .../pipelines/billing_attribution.yaml | 32 + .../configs/pipelines/billing_carriers.yaml | 18 + .../pipelines/billing_chargebacks.yaml | 26 + .../configs/pipelines/billing_forecast.yaml | 31 + .../pipelines/billing_fraud_scores.yaml | 33 + .../configs/pipelines/billing_orders.yaml | 33 + .../configs/pipelines/billing_promotions.yaml | 35 + .../configs/pipelines/billing_sessions.yaml | 24 + .../configs/pipelines/billing_spend.yaml | 28 + .../configs/pipelines/billing_spend_eu.yaml | 27 + .../configs/pipelines/billing_tickets.yaml | 26 + .../configs/pipelines/billing_warehouses.yaml | 27 + .../configs/pipelines/catalog_campaigns.yaml | 26 + .../pipelines/catalog_chargebacks.yaml | 33 + .../configs/pipelines/catalog_cohorts.yaml | 30 + .../configs/pipelines/catalog_forecast.yaml | 23 + .../configs/pipelines/catalog_orders.yaml | 31 + .../catalog_orders_backfill_safe.yaml | 24 + .../configs/pipelines/catalog_prices.yaml | 38 + .../configs/pipelines/catalog_products.yaml | 21 + .../configs/pipelines/catalog_promotions.yaml | 19 + .../configs/pipelines/catalog_sessions.yaml | 18 + .../configs/pipelines/catalog_signups.yaml | 24 + .../configs/pipelines/catalog_tickets.yaml | 26 + .../pipelines/finance_attribution.yaml | 23 + .../pipelines/finance_chargebacks.yaml | 32 + .../configs/pipelines/finance_clicks.yaml | 18 + .../pipelines/finance_fraud_scores.yaml | 18 + .../configs/pipelines/finance_inventory.yaml | 19 + .../pipelines/finance_inventory_us.yaml | 23 + .../configs/pipelines/finance_ledger.yaml | 24 + .../configs/pipelines/finance_payouts.yaml | 30 + .../configs/pipelines/finance_promotions.yaml | 20 + .../configs/pipelines/finance_sessions.yaml | 29 + .../configs/pipelines/finance_tickets.yaml | 39 + .../finance_tickets_backfill_safe.yaml | 19 + .../configs/pipelines/growth_ledger.yaml | 18 + .../configs/pipelines/growth_orders.yaml | 33 + .../growth_orders_hourly_rollup.yaml | 20 + .../configs/pipelines/growth_orders_v2.yaml | 32 + .../configs/pipelines/growth_payouts.yaml | 23 + .../configs/pipelines/growth_prices.yaml | 36 + .../configs/pipelines/growth_products.yaml | 23 + .../configs/pipelines/growth_queries.yaml | 23 + .../configs/pipelines/growth_refunds.yaml | 31 + .../growth_refunds_backfill_safe.yaml | 35 + .../growth_refunds_hourly_rollup.yaml | 38 + .../configs/pipelines/growth_spend.yaml | 28 + .../configs/pipelines/logistics_clicks.yaml | 36 + .../configs/pipelines/logistics_payouts.yaml | 25 + .../configs/pipelines/logistics_payroll.yaml | 28 + .../configs/pipelines/logistics_prices.yaml | 30 + .../pipelines/logistics_promotions.yaml | 39 + .../configs/pipelines/logistics_queries.yaml | 19 + .../pipelines/logistics_settlements.yaml | 23 + .../pipelines/logistics_shipments.yaml | 28 + .../configs/pipelines/logistics_signups.yaml | 26 + .../pipelines/logistics_signups_daily.yaml | 19 + .../pipelines/logistics_suppliers.yaml | 26 + .../pipelines/logistics_suppliers_daily.yaml | 37 + .../pipelines/marketing_campaigns.yaml | 18 + .../configs/pipelines/marketing_forecast.yaml | 37 + .../configs/pipelines/marketing_prices.yaml | 33 + .../configs/pipelines/marketing_queries.yaml | 24 + .../pipelines/marketing_queries_v2.yaml | 36 + .../configs/pipelines/marketing_refunds.yaml | 18 + .../configs/pipelines/marketing_sessions.yaml | 22 + .../pipelines/marketing_sessions_daily.yaml | 21 + .../pipelines/marketing_settlements.yaml | 22 + .../pipelines/marketing_suppliers.yaml | 28 + .../configs/pipelines/marketing_tickets.yaml | 31 + .../marketing_tickets_hourly_rollup.yaml | 32 + .../configs/pipelines/payments_campaigns.yaml | 38 + .../configs/pipelines/payments_carriers.yaml | 18 + .../configs/pipelines/payments_csat.yaml | 37 + .../configs/pipelines/payments_csat_eu.yaml | 19 + .../configs/pipelines/payments_headcount.yaml | 33 + .../configs/pipelines/payments_products.yaml | 23 + .../pipelines/payments_promotions.yaml | 26 + .../pipelines/payments_promotions_us.yaml | 19 + .../configs/pipelines/payments_refunds.yaml | 28 + .../configs/pipelines/payments_sessions.yaml | 27 + .../configs/pipelines/payments_spend.yaml | 23 + .../configs/pipelines/people_attribution.yaml | 21 + .../configs/pipelines/people_carriers.yaml | 21 + .../configs/pipelines/people_chargebacks.yaml | 27 + .../people_chargebacks_backfill_safe.yaml | 27 + .../configs/pipelines/people_clicks.yaml | 18 + .../configs/pipelines/people_clicks_eu.yaml | 23 + .../configs/pipelines/people_csat.yaml | 37 + .../configs/pipelines/people_payouts.yaml | 31 + .../configs/pipelines/people_prices.yaml | 30 + .../configs/pipelines/people_products.yaml | 24 + .../configs/pipelines/people_sessions.yaml | 23 + .../configs/pipelines/risk_carriers.yaml | 24 + .../configs/pipelines/risk_cohorts.yaml | 36 + .../configs/pipelines/risk_fraud_scores.yaml | 32 + .../configs/pipelines/risk_headcount.yaml | 20 + .../configs/pipelines/risk_orders.yaml | 31 + .../configs/pipelines/risk_orders_daily.yaml | 24 + .../configs/pipelines/risk_products.yaml | 30 + .../configs/pipelines/risk_refunds.yaml | 24 + .../fixture/configs/pipelines/risk_spend.yaml | 28 + .../configs/pipelines/risk_spend_eu.yaml | 25 + .../configs/pipelines/risk_tickets.yaml | 19 + .../configs/pipelines/risk_warehouses.yaml | 33 + .../configs/pipelines/search_attribution.yaml | 32 + .../configs/pipelines/search_campaigns.yaml | 24 + .../configs/pipelines/search_chargebacks.yaml | 32 + .../configs/pipelines/search_forecast.yaml | 18 + .../configs/pipelines/search_inventory.yaml | 35 + .../configs/pipelines/search_products.yaml | 31 + .../configs/pipelines/search_refunds.yaml | 27 + .../configs/pipelines/search_signups.yaml | 31 + .../search_signups_backfill_safe.yaml | 37 + .../configs/pipelines/search_signups_eu.yaml | 35 + .../configs/pipelines/search_suppliers.yaml | 24 + .../configs/pipelines/supply_attribution.yaml | 19 + .../configs/pipelines/supply_chargebacks.yaml | 26 + .../supply_chargebacks_hourly_rollup.yaml | 25 + .../configs/pipelines/supply_clicks.yaml | 32 + .../configs/pipelines/supply_cohorts.yaml | 21 + .../configs/pipelines/supply_orders.yaml | 22 + .../configs/pipelines/supply_returns.yaml | 36 + .../configs/pipelines/supply_returns_us.yaml | 23 + .../configs/pipelines/supply_sessions.yaml | 32 + .../configs/pipelines/supply_sessions_us.yaml | 30 + .../configs/pipelines/supply_spend.yaml | 31 + .../configs/pipelines/supply_tickets.yaml | 35 + .../configs/pipelines/support_carriers.yaml | 31 + .../pipelines/support_chargebacks.yaml | 31 + .../configs/pipelines/support_clicks.yaml | 34 + .../configs/pipelines/support_forecast.yaml | 32 + .../support_forecast_backfill_safe.yaml | 23 + .../pipelines/support_forecast_daily.yaml | 36 + .../pipelines/support_forecast_v2.yaml | 30 + .../pipelines/support_fraud_scores.yaml | 34 + .../configs/pipelines/support_inventory.yaml | 18 + .../configs/pipelines/support_payroll.yaml | 28 + .../configs/pipelines/support_sessions.yaml | 23 + .../fixture/configs/registry.yaml | 20273 ++++++++++++++++ .../fixture/dags/pipeline_factory.py | 96 + .../fixture/pools.json | 3 + .../fixture/requirements.txt | 2 + .../hard-reliability-factory-fleet/grade.py | 495 + .../dags/pipeline_factory.py | 140 + .../dags/pipeline_factory.py | 139 + .../index_minute/dags/pipeline_factory.py | 141 + .../minute_zero/dags/pipeline_factory.py | 140 + .../dags/pipeline_factory.py | 138 + .../dags/pipeline_factory.py | 138 + .../now_doc_md/dags/pipeline_factory.py | 140 + .../outer_try/dags/pipeline_factory.py | 140 + .../dags/pipeline_factory.py | 140 + .../mutants/pool_not_declared/pools.json | 3 + .../dags/pipeline_factory.py | 140 + .../dags/pipeline_factory.py | 139 + .../set_owner/dags/pipeline_factory.py | 140 + .../silent_skip/dags/pipeline_factory.py | 139 + .../reference/dags/pipeline_factory.py | 140 + .../reference/pools.json | 4 + .../alt_reference/dags/hourly_pageviews.py | 98 + .../case.yaml | 28 + .../fixture/.gitignore | 2 + .../fixture/README.md | 13 + .../fixture/dags/hourly_pageviews.py | 110 + .../fixture/data/pageviews.jsonl | 787 + .../fixture/requirements.txt | 2 + .../grade.py | 104 + .../migsim.py | 260 + .../dags/hourly_pageviews.py | 112 + .../dags/hourly_pageviews.py | 110 + .../dags/hourly_pageviews.py | 110 + .../ruff_fix_only/dags/hourly_pageviews.py | 110 + .../reference/dags/hourly_pageviews.py | 112 + .../alt_reference/dags/inventory_snapshot.py | 74 + .../migration-inventory-context/case.yaml | 25 + .../fixture/.gitignore | 2 + .../fixture/README.md | 10 + .../fixture/dags/inventory_snapshot.py | 93 + .../fixture/data/stock_movements.csv | 128 + .../fixture/requirements.txt | 1 + .../migration-inventory-context/grade.py | 86 + .../migration-inventory-context/migsim.py | 260 + .../dags/inventory_snapshot.py | 87 + .../dags/inventory_snapshot.py | 88 + .../dags/inventory_snapshot.py | 87 + .../dags/inventory_snapshot.py | 89 + .../reference/dags/inventory_snapshot.py | 89 + .../alt_reference/_DELETE | 1 + .../alt_reference/dags/daily_revenue.py | 73 + .../alt_reference/dags/region_reference.py | 31 + .../alt_reference/tests/test_dag_integrity.py | 23 + .../migration-revenue-previous-day/case.yaml | 28 + .../fixture/README.md | 15 + .../fixture/dags/daily_revenue.py | 71 + .../fixture/dags/region_reference.py | 36 + .../fixture/dags/sql/daily_revenue.sql | 21 + .../fixture/data/orders.csv | 73 + .../fixture/requirements.txt | 4 + .../fixture/tests/test_dag_integrity.py | 24 + .../migration-revenue-previous-day/grade.py | 133 + .../migration-revenue-previous-day/migsim.py | 260 + .../dags/sql/daily_revenue.sql | 21 + .../naive_cron_trigger/dags/daily_revenue.py | 71 + .../dags/sql/daily_revenue.sql | 21 + .../tests/test_dag_integrity.py | 24 + .../reference/dags/daily_revenue.py | 74 + .../reference/dags/region_reference.py | 36 + .../reference/dags/sql/daily_revenue.sql | 21 + .../reference/tests/test_dag_integrity.py | 25 + .../alt_reference/dags/carrier_scorecard.py | 64 + .../alt_reference/dags/ingest_shipments.py | 60 + .../migration-shipments-assets/case.yaml | 29 + .../fixture/.gitignore | 2 + .../fixture/README.md | 10 + .../fixture/dags/carrier_scorecard.py | 69 + .../fixture/dags/ingest_shipments.py | 74 + .../fixture/data/carrier_events.csv | 154 + .../fixture/requirements.txt | 2 + .../cases/migration-shipments-assets/grade.py | 147 + .../migration-shipments-assets/migsim.py | 260 + .../db_session_kept/dags/ingest_shipments.py | 73 + .../dags/ingest_shipments.py | 56 + .../dags/ingest_shipments.py | 56 + .../dags/carrier_scorecard.py | 68 + .../reference/dags/carrier_scorecard.py | 68 + .../reference/dags/ingest_shipments.py | 61 + .../alt_reference/dags/campaign_sync.py | 31 + .../dags/weekly_campaign_report.py | 76 + .../cases/migration-weekly-catchup/case.yaml | 27 + .../fixture/README.md | 17 + .../fixture/dags/campaign_sync.py | 31 + .../fixture/dags/weekly_campaign_report.py | 63 + .../fixture/data/campaign_events.csv | 358 + .../fixture/data/campaigns.csv | 5 + .../fixture/requirements.txt | 2 + .../cases/migration-weekly-catchup/grade.py | 176 + .../cases/migration-weekly-catchup/migsim.py | 260 + .../dags/weekly_campaign_report.py | 81 + .../dags/weekly_campaign_report.py | 79 + .../dags/weekly_campaign_report.py | 83 + .../dags/weekly_campaign_report.py | 74 + .../reference/dags/campaign_sync.py | 31 + .../reference/dags/weekly_campaign_report.py | 83 + .../alt_reference/dags/pageviews_daily.py | 57 + .../scheduling-backfill-pageviews/case.yaml | 41 + .../fixture/README.md | 19 + .../fixture/dags/pageviews_daily.py | 66 + .../fixture/dags/top_pages_weekly.py | 51 + .../fixture/data/pageviews/2026-09-01.csv | 33 + .../fixture/data/pageviews/2026-09-02.csv | 27 + .../fixture/data/pageviews/2026-09-03.csv | 23 + .../fixture/data/pageviews/2026-09-04.csv | 30 + .../fixture/data/pageviews/2026-09-05.csv | 44 + .../fixture/data/pageviews/2026-09-06.csv | 30 + .../fixture/data/pageviews/2026-09-07.csv | 43 + .../fixture/data/pageviews/2026-09-08.csv | 30 + .../fixture/data/pageviews/2026-09-09.csv | 44 + .../fixture/data/pageviews/2026-09-10.csv | 44 + .../fixture/data/pageviews/2026-09-11.csv | 20 + .../fixture/data/pageviews/2026-09-12.csv | 35 + .../fixture/data/pageviews/2026-09-13.csv | 33 + .../fixture/data/pageviews/2026-09-14.csv | 25 + .../fixture/data/pageviews/2026-09-15.csv | 33 + .../fixture/data/pageviews/2026-09-16.csv | 40 + .../fixture/data/pageviews/2026-09-17.csv | 39 + .../fixture/data/pageviews/2026-09-18.csv | 33 + .../fixture/data/pageviews/2026-09-19.csv | 38 + .../fixture/data/pageviews/2026-09-20.csv | 47 + .../fixture/data/pageviews/2026-09-21.csv | 46 + .../fixture/data/pageviews/2026-09-22.csv | 45 + .../fixture/data/pageviews/2026-09-23.csv | 19 + .../fixture/data/pageviews/2026-09-24.csv | 45 + .../fixture/data/pageviews/2026-09-25.csv | 25 + .../fixture/data/pageviews/2026-09-26.csv | 41 + .../fixture/data/pageviews/2026-09-27.csv | 21 + .../fixture/data/pageviews/2026-09-28.csv | 30 + .../fixture/data/pageviews/2026-09-29.csv | 30 + .../fixture/requirements.txt | 2 + .../scheduling-backfill-pageviews/grade.py | 376 + .../dags/pageviews_daily.py | 72 + .../dags/pageviews_daily.py | 72 + .../dags/pageviews_daily.py | 73 + .../dags/pageviews_daily.py | 56 + .../dags/pageviews_daily.py | 71 + .../dags/pageviews_daily.py | 76 + .../skip_loaded_days/dags/pageviews_daily.py | 77 + .../dags/pageviews_daily.py | 72 + .../reference/dags/pageviews_daily.py | 72 + .../alt_reference/dags/orders_daily_load.py | 85 + .../case.yaml | 34 + .../fixture/README.md | 16 + .../fixture/dags/customers_snapshot.py | 37 + .../fixture/dags/orders_daily_load.py | 89 + .../fixture/data/customers.csv | 41 + .../fixture/data/orders.csv | 131 + .../fixture/requirements.txt | 3 + .../grade.py | 234 + .../dags/orders_daily_load.py | 104 + .../dags/orders_daily_load.py | 102 + .../dags/orders_daily_load.py | 106 + .../dags/orders_daily_load.py | 100 + .../dags/orders_daily_load.py | 111 + .../dags/orders_daily_load.py | 102 + .../reference/dags/orders_daily_load.py | 104 + .../alt_reference/dags/fx_pnl_report.py | 55 + .../case.yaml | 34 + .../fixture/README.md | 11 + .../fixture/dags/desk_reference_refresh.py | 31 + .../fixture/dags/fx_pnl_report.py | 58 + .../fixture/data/desks.csv | 4 + .../fixture/data/fx_trades.csv | 208 + .../fixture/requirements.txt | 1 + .../grade.py | 233 + .../calendar_yesterday/dags/fx_pnl_report.py | 71 + .../dags/fx_pnl_report.py | 72 + .../dags/fx_pnl_report.py | 71 + .../utc_eleven_am_cron/dags/fx_pnl_report.py | 71 + .../wall_clock_run_day/dags/fx_pnl_report.py | 71 + .../reference/dags/fx_pnl_report.py | 70 + .../tests/test_shipments_daily.py | 76 + .../cases/testing-airflow2-ci-suite/case.yaml | 39 + .../fixture/.gitignore | 3 + .../fixture/README.md | 16 + .../fixture/dags/carrier_rates_weekly.py | 41 + .../fixture/dags/shipments_daily.py | 89 + .../fixture/data/carrier_rates.csv | 6 + .../fixture/data/shipments.csv | 10 + .../fixture/requirements.txt | 1 + .../cases/testing-airflow2-ci-suite/grade.py | 175 + .../af3_import/dags/carrier_rates_weekly.py | 41 + .../dags/shipments_daily.py | 89 + .../retries_dropped/dags/shipments_daily.py | 87 + .../dags/shipments_daily.py | 91 + .../weekend_schedule/dags/shipments_daily.py | 89 + .../mutants/af3_test_apis/tests/test_dags.py | 95 + .../mutants/ast_snapshot/_DELETE | 2 + .../ast_snapshot/tests/test_dag_sources.py | 28 + .../schedule_time_only/tests/test_dags.py | 94 + .../mutants/source_snapshot/_DELETE | 2 + .../source_snapshot/tests/test_dag_sources.py | 20 + .../mutants/structure_only/tests/test_dags.py | 50 + .../reference/tests/test_dags.py | 95 + .../tests/test_csv_partition_operator.py | 67 + .../testing-custom-operator-conn/case.yaml | 36 + .../fixture/.gitignore | 3 + .../fixture/README.md | 14 + .../fixture/dags/.airflowignore | 1 + .../fixture/dags/load_raw_sales.py | 38 + .../fixture/dags/warehouse/__init__.py | 1 + .../fixture/dags/warehouse/hooks.py | 52 + .../fixture/dags/warehouse/operators.py | 63 + .../fixture/data/orders.csv | 7 + .../fixture/data/refunds.csv | 3 + .../fixture/requirements.txt | 2 + .../testing-custom-operator-conn/grade.py | 170 + .../dags/warehouse/operators.py | 63 + .../hook_ignores_host/dags/warehouse/hooks.py | 48 + .../dags/warehouse/operators.py | 63 + .../dags/warehouse/operators.py | 62 + .../schema_from_extra/dags/warehouse/hooks.py | 52 + .../mutants/ast_snapshot/_DELETE | 3 + .../ast_snapshot/tests/test_dag_sources.py | 30 + .../tests/test_warehouse.py | 32 + .../mocked_duckdb/tests/test_warehouse.py | 29 + .../patched_conn_any_id/tests/conftest.py | 7 + .../tests/test_warehouse.py | 79 + .../return_value_only/tests/test_warehouse.py | 25 + .../mutants/source_snapshot/_DELETE | 3 + .../source_snapshot/tests/test_dag_sources.py | 22 + .../reference/tests/conftest.py | 7 + .../reference/tests/test_warehouse.py | 83 + .../alt_reference/tests/test_dags.py | 51 + .../cases/testing-dagbag-integrity/case.yaml | 33 + .../fixture/.gitignore | 4 + .../fixture/README.md | 14 + .../fixture/dags/customer_snapshot.py | 52 + .../fixture/dags/marketing_spend.py | 54 + .../fixture/dags/orders_ingest.py | 74 + .../fixture/data/customers.csv | 5 + .../fixture/data/marketing_spend.csv | 6 + .../fixture/data/orders.csv | 7 + .../fixture/requirements.txt | 2 + .../cases/testing-dagbag-integrity/grade.py | 175 + .../catchup_enabled/dags/customer_snapshot.py | 52 + .../dag_cycle/dags/orders_ingest.py | 77 + .../import_error/dags/customer_snapshot.py | 52 + .../missing_dependency/dags/orders_ingest.py | 76 + .../retries_disabled/dags/marketing_spend.py | 54 + .../tests/test_dag_integrity.py | 51 + .../mutants/ast_snapshot/_DELETE | 2 + .../ast_snapshot/tests/test_dag_sources.py | 29 + .../tests/test_dag_integrity.py | 53 + .../tests/test_dag_integrity.py | 21 + .../mutants/source_snapshot/_DELETE | 2 + .../source_snapshot/tests/test_dag_sources.py | 21 + .../reference/tests/test_dag_integrity.py | 53 + .../tests/test_enrichment_pipeline.py | 95 + .../testing-taskflow-transforms/case.yaml | 37 + .../fixture/.gitignore | 3 + .../fixture/README.md | 15 + .../fixture/dags/orders_enrichment.py | 98 + .../fixture/data/fx_rates.csv | 7 + .../fixture/data/orders.csv | 10 + .../fixture/requirements.txt | 1 + .../testing-taskflow-transforms/grade.py | 170 + .../dags/orders_enrichment.py | 98 + .../dags/orders_enrichment.py | 98 + .../dags/orders_enrichment.py | 98 + .../dags/orders_enrichment.py | 97 + .../dags/orders_enrichment.py | 99 + .../mutants/ast_snapshot/_DELETE | 3 + .../ast_snapshot/tests/test_dag_sources.py | 27 + .../calls_task_not_function/tests/conftest.py | 6 + .../tests/test_orders_enrichment.py | 81 + .../schedule_interval_attr/tests/conftest.py | 6 + .../tests/test_orders_enrichment.py | 81 + .../mutants/source_snapshot/_DELETE | 3 + .../source_snapshot/tests/test_dag_sources.py | 19 + .../transforms_and_e2e_only/tests/conftest.py | 6 + .../tests/test_orders_enrichment.py | 68 + .../mutants/wiring_only/tests/conftest.py | 6 + .../tests/test_orders_enrichment.py | 33 + .../reference/tests/conftest.py | 6 + .../reference/tests/test_orders_enrichment.py | 81 + .../REPORT.md | 138 + .../REVIEW.md | 73 + .../all/REPORT.md | 246 + .../all/results.json | 6165 +++++ .../contamination.jsonl | 244 + .../excluding-flagged/REPORT.md | 131 + .../excluding-flagged/results.json | 2274 ++ .../excluding-strong/REPORT.md | 225 + .../excluding-strong/results.json | 5246 ++++ .../meta.json | 163 + .../results.json | 3696 +++ .../inventory-baseline.json | 405 + .../results/baseline-2026-09-30/meta.json | 348 + .../baseline-2026-09-30/report/REPORT.md | 75 + .../baseline-2026-09-30/report/results.json | 1256 + .../results/baseline-2026-09-30/runs.jsonl | 120 + .../run-1/attempt-0/agent.patch | 106 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 106 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 92 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 101 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 98 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 0 .../run-2/attempt-0/attempt.json | 28 + .../run-2/attempt-1/agent.patch | 285 + .../run-2/attempt-1/attempt.json | 32 + .../run-2/attempt-1/final.md | 1 + .../run-2/attempt-1/grade.json | 67 + .../run-1/attempt-0/agent.patch | 161 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 192 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 101 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 103 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 78 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 76 + .../run-2/attempt-0/attempt.json | 38 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 133 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 37 + .../run-2/attempt-0/agent.patch | 298 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 126 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 131 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 110 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 97 + .../run-2/attempt-0/agent.patch | 242 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 130 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 97 + .../run-2/attempt-0/agent.patch | 111 + .../run-2/attempt-0/attempt.json | 38 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 22 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 28 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 47 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 104 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 74 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 67 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 81 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 52 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 43 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 37 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 37 + .../run-2/attempt-0/agent.patch | 37 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 37 + .../run-1/attempt-0/agent.patch | 73 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 201 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 60 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 59 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 32 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 55 + .../run-2/attempt-0/agent.patch | 492 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 80 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 55 + .../run-2/attempt-0/agent.patch | 63 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 133 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 249 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 101 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 110 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 372 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 49 + .../run-2/attempt-0/agent.patch | 277 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 49 + .../run-1/attempt-0/agent.patch | 132 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 138 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 72 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 180 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 132 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 89 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 571 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 90 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 82 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 87 + .../run-2/attempt-0/attempt.json | 43 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 137 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 1064 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 178 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 102 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 140 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 37 + .../run-2/attempt-0/agent.patch | 156 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 58 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 57 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 553 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 540 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 572 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 580 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 651 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 687 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 378 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 416 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 296 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 181 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 226 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 186 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 576 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 545 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 490 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 481 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../selftest/selftest.json | 2169 ++ .../results/baseline-n3-2026-09-30/REPORT.md | 78 + .../baseline-n3-2026-09-30/results.json | 1593 ++ .../inventory-baseline.json | 405 + .../baseline-run3-2026-09-30/meta.json | 387 + .../baseline-run3-2026-09-30/runs.jsonl | 40 + .../run-3/attempt-0/agent.patch | 107 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 100 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 88 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 146 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 84 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 95 + .../run-3/attempt-0/attempt.json | 39 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 115 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 0 .../run-3/attempt-0/attempt.json | 34 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-1/agent.patch | 121 + .../run-3/attempt-1/attempt.json | 32 + .../run-3/attempt-1/final.md | 1 + .../run-3/attempt-1/grade.json | 79 + .../run-3/attempt-0/agent.patch | 94 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 97 + .../run-3/attempt-0/agent.patch | 119 + .../run-3/attempt-0/attempt.json | 38 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 97 + .../run-3/attempt-0/agent.patch | 28 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 77 + .../run-3/attempt-0/attempt.json | 38 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 57 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 51 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 49 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 40 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 37 + .../run-3/attempt-0/agent.patch | 57 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 75 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 464 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 55 + .../run-3/attempt-0/agent.patch | 114 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 55 + .../run-3/attempt-0/agent.patch | 362 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 110 + .../run-3/attempt-0/attempt.json | 38 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 175 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 49 + .../run-3/attempt-0/agent.patch | 108 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 409 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 114 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 98 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 37 + .../run-3/attempt-0/agent.patch | 79 + .../run-3/attempt-0/attempt.json | 43 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 107 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 95 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 56 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 70 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 37 + .../run-3/attempt-0/agent.patch | 495 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 444 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 528 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 655 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 838 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 189 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 697 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 831 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../inventory-baseline.json | 194 + .../meta.json | 370 + .../runs.jsonl | 29 + .../run-1/attempt-0/agent.patch | 82 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 10 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 78 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 10 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 123 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 13 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 121 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 13 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 68 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 9 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 65 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 13 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 85 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 11 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 93 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 10 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 86 + .../run-1/attempt-0/attempt.json | 154 + .../run-1/attempt-0/final.md | 13 + .../run-1/attempt-0/grade.json | 97 + .../run-2/attempt-0/agent.patch | 85 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 9 + .../run-2/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 66 + .../run-1/attempt-0/attempt.json | 142 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 70 + .../run-2/attempt-0/attempt.json | 148 + .../run-2/attempt-0/final.md | 17 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 68 + .../run-1/attempt-0/attempt.json | 154 + .../run-1/attempt-0/final.md | 27 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 88 + .../run-2/attempt-0/attempt.json | 154 + .../run-2/attempt-0/final.md | 22 + .../run-2/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 44 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 11 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 45 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 14 + .../run-2/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 73 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 15 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 79 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 125 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 109 + .../run-2/attempt-0/attempt.json | 142 + .../run-2/attempt-0/final.md | 24 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 145 + .../run-2/attempt-0/final.md | 22 + .../run-2/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 90 + .../run-1/attempt-0/attempt.json | 160 + .../run-1/attempt-0/final.md | 13 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 84 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 78 + .../run-1/attempt-0/attempt.json | 149 + .../run-1/attempt-0/final.md | 16 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 59 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 10 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 189 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 179 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 109 + .../run-1/attempt-0/attempt.json | 154 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 207 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 79 + .../results/cc-campaign-2026-10-01.state | 5 + .../inventory-skill.json | 209 + .../cc-skill-sonnet-5-5-2026-10-01/meta.json | 397 + .../cc-skill-sonnet-5-5-2026-10-01/runs.jsonl | 20 + .../run-1/attempt-0/agent.patch | 87 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 13 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 117 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 84 + .../run-1/attempt-0/attempt.json | 165 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 91 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 91 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 76 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 94 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/final.md | 32 + .../run-1/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 58 + .../run-1/attempt-0/attempt.json | 184 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 71 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 156 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 149 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 81 + .../run-1/attempt-0/attempt.json | 166 + .../run-1/attempt-0/final.md | 26 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 43 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 74 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 68 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 168 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 196 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 40 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 117 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 26 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 267 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/final.md | 37 + .../run-1/attempt-0/grade.json | 79 + .../inventory-skill.json | 209 + .../results/cc-triggers-2026-10-01/meta.json | 260 + .../results/cc-triggers-2026-10-01/runs.jsonl | 116 + .../run-1/attempt-1/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-1/attempt.json | 154 + .../run-2/attempt-0/attempt.json | 154 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 153 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 160 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 171 + .../run-1/attempt-0/attempt.json | 154 + .../run-2/attempt-0/attempt.json | 155 + .../run-1/attempt-0/attempt.json | 151 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 147 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 154 + .../run-2/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 147 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 136 + .../run-1/attempt-0/final.md | 3 + .../run-2/attempt-0/attempt.json | 147 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 171 + .../run-2/attempt-0/attempt.json | 159 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 177 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 171 + .../run-2/attempt-0/attempt.json | 171 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 172 + .../run-1/attempt-0/attempt.json | 173 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 165 + .../run-1/attempt-0/attempt.json | 146 + .../run-2/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 154 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 159 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 134 + .../claude-fable-5-1/run-1/attempt-0/final.md | 16 + .../run-1/attempt-0/attempt.json | 134 + .../claude-opus-5-5/run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/attempt.json | 133 + .../run-1/attempt-0/final.md | 44 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 152 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 9 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 135 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-1/attempt-0/attempt.json | 136 + .../run-1/attempt-0/final.md | 14 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 135 + .../claude-opus-5-5/run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 7 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 158 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 8 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 135 + .../claude-opus-5-5/run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/attempt.json | 116 + .../run-1/attempt-0/attempt.json | 141 + .../claude-fable-5-1/run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/attempt.json | 141 + .../claude-opus-5-5/run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 141 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 131 + .../claude-fable-5-1/run-1/attempt-0/final.md | 15 + .../run-1/attempt-0/attempt.json | 131 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-1/attempt-0/attempt.json | 131 + .../run-1/attempt-0/final.md | 31 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/attempt.json | 134 + .../run-1/attempt-0/final.md | 5 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 147 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 147 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 154 + .../run-1/attempt-0/attempt.json | 147 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 146 + .../run-1/attempt-0/attempt.json | 149 + .../results/final-campaign-2026-10-01.state | 44 + .../inventory-baseline.json | 194 + .../meta.json | 462 + .../runs.jsonl | 20 + .../run-1/attempt-0/agent.patch | 83 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 152 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 14 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 83 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 116 + .../run-1/attempt-0/attempt.json | 159 + .../claude-opus-5-5/run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 108 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 64 + .../run-1/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-1/attempt-0/final.md | 25 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 91 + .../run-1/attempt-0/attempt.json | 162 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 46 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 74 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 76 + .../run-1/attempt-0/attempt.json | 192 + .../claude-opus-5-5/run-1/attempt-0/final.md | 15 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 142 + .../run-1/attempt-0/attempt.json | 150 + .../claude-opus-5-5/run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 166 + .../run-1/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 124 + .../run-1/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-1/attempt-0/final.md | 16 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 62 + .../run-1/attempt-0/attempt.json | 159 + .../claude-opus-5-5/run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 207 + .../run-1/attempt-0/attempt.json | 146 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 58 + .../run-1/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 272 + .../run-1/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 275 + .../run-1/attempt-0/attempt.json | 156 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 149 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 396 + .../run-1/attempt-0/attempt.json | 162 + .../claude-opus-5-5/run-1/attempt-0/final.md | 34 + .../run-1/attempt-0/grade.json | 79 + .../inventory-baseline.json | 194 + .../meta.json | 462 + .../runs.jsonl | 20 + .../run-1/attempt-0/agent.patch | 79 + .../run-1/attempt-0/attempt.json | 151 + .../run-1/attempt-0/final.md | 10 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 106 + .../run-1/attempt-0/attempt.json | 144 + .../run-1/attempt-0/final.md | 14 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 3599 +++ .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 11 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 86 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 15 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 84 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 11 + .../run-1/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 65 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 16 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 80 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 44 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 74 + .../run-1/attempt-0/attempt.json | 144 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 157 + .../run-1/attempt-0/final.md | 12 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 125 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 120 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 79 + .../run-1/attempt-0/attempt.json | 144 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 54 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 10 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 124 + .../run-1/attempt-0/attempt.json | 144 + .../run-1/attempt-0/final.md | 11 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 81 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 191 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 16 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 164 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 108 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 245 + .../run-1/attempt-0/attempt.json | 150 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 79 + .../inventory-skill.json | 209 + .../meta.json | 474 + .../runs.jsonl | 20 + .../run-1/attempt-0/agent.patch | 84 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 146 + .../run-1/attempt-0/attempt.json | 155 + .../claude-opus-5-5/run-1/attempt-0/final.md | 31 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 81 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 124 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 30 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 110 + .../run-1/attempt-0/attempt.json | 155 + .../claude-opus-5-5/run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 71 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 32 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 95 + .../run-1/attempt-0/attempt.json | 173 + .../claude-opus-5-5/run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 3606 +++ .../run-1/attempt-0/attempt.json | 155 + .../claude-opus-5-5/run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 125 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 92 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 27 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 239 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 32 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 146 + .../run-1/attempt-0/attempt.json | 129 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 102 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 26 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 94 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 2800 +++ .../run-1/attempt-0/attempt.json | 129 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 76 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 32 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 257 + .../run-1/attempt-0/attempt.json | 161 + .../claude-opus-5-5/run-1/attempt-0/final.md | 30 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 229 + .../run-1/attempt-0/attempt.json | 161 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 144 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 377 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-1/attempt-0/grade.json | 79 + .../inventory-skill.json | 209 + .../meta.json | 474 + .../runs.jsonl | 20 + .../run-1/attempt-0/agent.patch | 80 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 113 + .../run-1/attempt-0/attempt.json | 155 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 61 + .../run-1/attempt-0/attempt.json | 161 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 87 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 90 + .../run-1/attempt-0/attempt.json | 183 + .../run-1/attempt-0/final.md | 14 + .../run-1/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 71 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 174 + .../run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 72 + .../run-1/attempt-0/attempt.json | 167 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 78 + .../run-1/attempt-0/attempt.json | 162 + .../run-1/attempt-0/final.md | 25 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 93 + .../run-1/attempt-0/attempt.json | 161 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 177 + .../run-1/attempt-0/attempt.json | 155 + .../run-1/attempt-0/final.md | 30 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 115 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 98 + .../run-1/attempt-0/attempt.json | 161 + .../run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 77 + .../run-1/attempt-0/attempt.json | 155 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 60 + .../run-1/attempt-0/attempt.json | 161 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 230 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 193 + .../run-1/attempt-0/attempt.json | 161 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 117 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 303 + .../run-1/attempt-0/attempt.json | 164 + .../run-1/attempt-0/final.md | 20 + .../run-1/attempt-0/grade.json | 79 + .../inventory-baseline.json | 187 + .../meta.json | 333 + .../runs.jsonl | 12 + .../run-1/attempt-0/agent.patch | 643 + .../run-1/attempt-0/attempt.json | 209 + .../claude-fable-5-1/run-1/attempt-0/final.md | 19 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 62 + .../run-2/attempt-0/attempt.json | 160 + .../claude-fable-5-1/run-2/attempt-0/final.md | 1 + .../run-2/attempt-1/agent.patch | 0 .../run-2/attempt-1/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-1/final.md | 1 + .../run-2/attempt-2/agent.patch | 0 .../run-2/attempt-2/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-2/final.md | 1 + .../run-2/attempt-3/attempt.json | 98 + .../run-1/attempt-0/agent.patch | 786 + .../run-1/attempt-0/attempt.json | 185 + .../claude-fable-5-1/run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 269 + .../run-2/attempt-0/attempt.json | 215 + .../claude-fable-5-1/run-2/attempt-0/final.md | 1 + .../run-2/attempt-1/agent.patch | 0 .../run-2/attempt-1/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-1/final.md | 1 + .../run-2/attempt-2/agent.patch | 0 .../run-2/attempt-2/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-2/final.md | 1 + .../run-2/attempt-3/attempt.json | 98 + .../run-1/attempt-0/agent.patch | 372 + .../run-1/attempt-0/attempt.json | 208 + .../claude-fable-5-1/run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 0 .../run-2/attempt-0/attempt.json | 160 + .../claude-fable-5-1/run-2/attempt-0/final.md | 1 + .../run-2/attempt-1/agent.patch | 0 .../run-2/attempt-1/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-1/final.md | 1 + .../run-2/attempt-2/agent.patch | 0 .../run-2/attempt-2/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-2/final.md | 1 + .../run-2/attempt-3/attempt.json | 98 + .../run-1/attempt-0/agent.patch | 490 + .../run-1/attempt-0/attempt.json | 239 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 0 .../run-2/attempt-0/attempt.json | 166 + .../claude-fable-5-1/run-2/attempt-0/final.md | 1 + .../run-2/attempt-1/agent.patch | 0 .../run-2/attempt-1/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-1/final.md | 1 + .../run-2/attempt-2/agent.patch | 0 .../run-2/attempt-2/attempt.json | 124 + .../claude-fable-5-1/run-2/attempt-2/final.md | 1 + .../run-2/attempt-3/attempt.json | 98 + .../run-1/attempt-0/agent.patch | 493 + .../run-1/attempt-0/attempt.json | 239 + .../run-1/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 252 + .../run-1/attempt-0/attempt.json | 150 + .../claude-fable-5-1/run-1/attempt-0/final.md | 35 + .../run-1/attempt-0/grade.json | 121 + .../inventory-baseline.json | 194 + .../meta.json | 320 + .../runs.jsonl | 18 + .../run-1/attempt-0/agent.patch | 250 + .../run-1/attempt-0/attempt.json | 193 + .../claude-opus-5-5/run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 242 + .../run-2/attempt-0/attempt.json | 162 + .../claude-opus-5-5/run-2/attempt-0/final.md | 27 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 254 + .../run-3/attempt-0/attempt.json | 153 + .../claude-opus-5-5/run-3/attempt-0/final.md | 27 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 247 + .../run-1/attempt-0/attempt.json | 206 + .../claude-opus-5-5/run-1/attempt-0/final.md | 30 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 310 + .../run-2/attempt-0/attempt.json | 167 + .../claude-opus-5-5/run-2/attempt-0/final.md | 26 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 320 + .../run-3/attempt-0/attempt.json | 171 + .../claude-opus-5-5/run-3/attempt-0/final.md | 20 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 318 + .../run-1/attempt-0/attempt.json | 170 + .../claude-opus-5-5/run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 311 + .../run-2/attempt-0/attempt.json | 159 + .../claude-opus-5-5/run-2/attempt-0/final.md | 25 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 268 + .../run-3/attempt-0/attempt.json | 179 + .../claude-opus-5-5/run-3/attempt-0/final.md | 23 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 256 + .../run-1/attempt-0/attempt.json | 161 + .../claude-opus-5-5/run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 269 + .../run-2/attempt-0/attempt.json | 159 + .../claude-opus-5-5/run-2/attempt-0/final.md | 25 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 315 + .../run-3/attempt-0/attempt.json | 146 + .../claude-opus-5-5/run-3/attempt-0/final.md | 23 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 360 + .../run-1/attempt-0/attempt.json | 146 + .../claude-opus-5-5/run-1/attempt-0/final.md | 25 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 465 + .../run-2/attempt-0/attempt.json | 161 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 500 + .../run-3/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-3/attempt-0/final.md | 22 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 288 + .../run-1/attempt-0/attempt.json | 150 + .../claude-opus-5-5/run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 121 + .../run-2/attempt-0/agent.patch | 251 + .../run-2/attempt-0/attempt.json | 204 + .../claude-opus-5-5/run-2/attempt-0/final.md | 26 + .../run-2/attempt-0/grade.json | 121 + .../run-3/attempt-0/agent.patch | 276 + .../run-3/attempt-0/attempt.json | 144 + .../claude-opus-5-5/run-3/attempt-0/final.md | 34 + .../run-3/attempt-0/grade.json | 121 + .../inventory-baseline.json | 194 + .../meta.json | 319 + .../runs.jsonl | 18 + .../run-1/attempt-0/agent.patch | 176 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 193 + .../run-2/attempt-0/attempt.json | 202 + .../run-2/attempt-0/final.md | 17 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 168 + .../run-3/attempt-0/attempt.json | 198 + .../run-3/attempt-0/final.md | 17 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 191 + .../run-1/attempt-0/attempt.json | 175 + .../run-1/attempt-0/final.md | 18 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 163 + .../run-2/attempt-0/attempt.json | 137 + .../run-2/attempt-0/final.md | 24 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 191 + .../run-3/attempt-0/attempt.json | 168 + .../run-3/attempt-0/final.md | 15 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 231 + .../run-1/attempt-0/attempt.json | 153 + .../run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 197 + .../run-2/attempt-0/attempt.json | 150 + .../run-2/attempt-0/final.md | 18 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 225 + .../run-3/attempt-0/attempt.json | 153 + .../run-3/attempt-0/final.md | 30 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 306 + .../run-1/attempt-0/attempt.json | 172 + .../run-1/attempt-0/final.md | 25 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 202 + .../run-2/attempt-0/attempt.json | 153 + .../run-2/attempt-0/final.md | 24 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 258 + .../run-3/attempt-0/attempt.json | 153 + .../run-3/attempt-0/final.md | 19 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 412 + .../run-1/attempt-0/attempt.json | 184 + .../run-1/attempt-0/final.md | 25 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 340 + .../run-2/attempt-0/attempt.json | 191 + .../run-2/attempt-0/final.md | 28 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 428 + .../run-3/attempt-0/attempt.json | 194 + .../run-3/attempt-0/final.md | 28 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 202 + .../run-1/attempt-0/attempt.json | 234 + .../run-1/attempt-0/final.md | 31 + .../run-1/attempt-0/grade.json | 121 + .../run-2/attempt-0/agent.patch | 208 + .../run-2/attempt-0/attempt.json | 162 + .../run-2/attempt-0/final.md | 25 + .../run-2/attempt-0/grade.json | 121 + .../run-3/attempt-0/agent.patch | 143 + .../run-3/attempt-0/attempt.json | 174 + .../run-3/attempt-0/final.md | 20 + .../run-3/attempt-0/grade.json | 121 + .../inventory-skill.json | 202 + .../meta.json | 325 + .../runs.jsonl | 12 + .../run-1/attempt-0/agent.patch | 324 + .../run-1/attempt-0/attempt.json | 159 + .../claude-fable-5-1/run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/agent.patch | 0 .../run-1/attempt-1/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-1/final.md | 1 + .../run-1/attempt-2/agent.patch | 0 .../run-1/attempt-2/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-2/final.md | 1 + .../run-1/attempt-3/agent.patch | 353 + .../run-1/attempt-3/attempt.json | 187 + .../claude-fable-5-1/run-1/attempt-3/final.md | 21 + .../run-1/attempt-3/grade.json | 79 + .../run-2/attempt-0/agent.patch | 352 + .../run-2/attempt-0/attempt.json | 204 + .../claude-fable-5-1/run-2/attempt-0/final.md | 17 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 3883 +++ .../run-1/attempt-0/attempt.json | 177 + .../claude-fable-5-1/run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/agent.patch | 0 .../run-1/attempt-1/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-1/final.md | 1 + .../run-1/attempt-2/agent.patch | 0 .../run-1/attempt-2/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-2/final.md | 1 + .../run-1/attempt-3/agent.patch | 295 + .../run-1/attempt-3/attempt.json | 240 + .../claude-fable-5-1/run-1/attempt-3/final.md | 36 + .../run-1/attempt-3/grade.json | 73 + .../run-2/attempt-0/agent.patch | 293 + .../run-2/attempt-0/attempt.json | 171 + .../claude-fable-5-1/run-2/attempt-0/final.md | 32 + .../run-2/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 315 + .../run-1/attempt-0/attempt.json | 152 + .../claude-fable-5-1/run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/agent.patch | 0 .../run-1/attempt-1/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-1/final.md | 1 + .../run-1/attempt-2/agent.patch | 0 .../run-1/attempt-2/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-2/final.md | 1 + .../run-1/attempt-3/agent.patch | 305 + .../run-1/attempt-3/attempt.json | 146 + .../claude-fable-5-1/run-1/attempt-3/final.md | 26 + .../run-1/attempt-3/grade.json | 73 + .../run-2/attempt-0/agent.patch | 385 + .../run-2/attempt-0/attempt.json | 164 + .../claude-fable-5-1/run-2/attempt-0/final.md | 27 + .../run-2/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 335 + .../run-1/attempt-0/attempt.json | 152 + .../claude-fable-5-1/run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/agent.patch | 0 .../run-1/attempt-1/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-1/final.md | 1 + .../run-1/attempt-2/agent.patch | 0 .../run-1/attempt-2/attempt.json | 129 + .../claude-fable-5-1/run-1/attempt-2/final.md | 1 + .../run-1/attempt-3/agent.patch | 372 + .../run-1/attempt-3/attempt.json | 146 + .../claude-fable-5-1/run-1/attempt-3/final.md | 21 + .../run-1/attempt-3/grade.json | 85 + .../run-2/attempt-0/agent.patch | 445 + .../run-2/attempt-0/attempt.json | 146 + .../claude-fable-5-1/run-2/attempt-0/final.md | 24 + .../run-2/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 400 + .../run-1/attempt-0/attempt.json | 146 + .../claude-fable-5-1/run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 362 + .../run-2/attempt-0/attempt.json | 149 + .../claude-fable-5-1/run-2/attempt-0/final.md | 25 + .../run-2/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 313 + .../run-1/attempt-0/attempt.json | 177 + .../claude-fable-5-1/run-1/attempt-0/final.md | 22 + .../run-1/attempt-0/grade.json | 121 + .../run-2/attempt-0/agent.patch | 301 + .../run-2/attempt-0/attempt.json | 198 + .../claude-fable-5-1/run-2/attempt-0/final.md | 39 + .../run-2/attempt-0/grade.json | 121 + .../inventory-skill.json | 209 + .../meta.json | 330 + .../runs.jsonl | 18 + .../run-1/attempt-0/agent.patch | 274 + .../run-1/attempt-0/attempt.json | 155 + .../claude-opus-5-5/run-1/attempt-0/final.md | 30 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 266 + .../run-2/attempt-0/attempt.json | 161 + .../claude-opus-5-5/run-2/attempt-0/final.md | 35 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 250 + .../run-3/attempt-0/attempt.json | 174 + .../claude-opus-5-5/run-3/attempt-0/final.md | 20 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 251 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 24 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 270 + .../run-2/attempt-0/attempt.json | 161 + .../claude-opus-5-5/run-2/attempt-0/final.md | 38 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 247 + .../run-3/attempt-0/attempt.json | 173 + .../claude-opus-5-5/run-3/attempt-0/final.md | 29 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 267 + .../run-1/attempt-0/attempt.json | 155 + .../claude-opus-5-5/run-1/attempt-0/final.md | 26 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 309 + .../run-2/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-2/attempt-0/final.md | 30 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 304 + .../run-3/attempt-0/attempt.json | 189 + .../claude-opus-5-5/run-3/attempt-0/final.md | 33 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 278 + .../run-1/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-1/attempt-0/final.md | 32 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 307 + .../run-2/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-2/attempt-0/final.md | 35 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 306 + .../run-3/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-3/attempt-0/final.md | 35 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 333 + .../run-1/attempt-0/attempt.json | 165 + .../claude-opus-5-5/run-1/attempt-0/final.md | 35 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 328 + .../run-2/attempt-0/attempt.json | 148 + .../claude-opus-5-5/run-2/attempt-0/final.md | 45 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 299 + .../run-3/attempt-0/attempt.json | 173 + .../claude-opus-5-5/run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 312 + .../run-1/attempt-0/attempt.json | 185 + .../claude-opus-5-5/run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 121 + .../run-2/attempt-0/agent.patch | 285 + .../run-2/attempt-0/attempt.json | 179 + .../claude-opus-5-5/run-2/attempt-0/final.md | 27 + .../run-2/attempt-0/grade.json | 121 + .../run-3/attempt-0/agent.patch | 261 + .../run-3/attempt-0/attempt.json | 185 + .../claude-opus-5-5/run-3/attempt-0/final.md | 44 + .../run-3/attempt-0/grade.json | 121 + .../inventory-skill.json | 209 + .../meta.json | 330 + .../runs.jsonl | 18 + .../run-1/attempt-0/agent.patch | 218 + .../run-1/attempt-0/attempt.json | 170 + .../run-1/attempt-0/final.md | 21 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 184 + .../run-2/attempt-0/attempt.json | 197 + .../run-2/attempt-0/final.md | 25 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 183 + .../run-3/attempt-0/attempt.json | 172 + .../run-3/attempt-0/final.md | 24 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 184 + .../run-1/attempt-0/attempt.json | 155 + .../run-1/attempt-0/final.md | 17 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 225 + .../run-2/attempt-0/attempt.json | 201 + .../run-2/attempt-0/final.md | 21 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 205 + .../run-3/attempt-0/attempt.json | 185 + .../run-3/attempt-0/final.md | 29 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 226 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 33 + .../run-1/attempt-0/grade.json | 73 + .../run-2/attempt-0/agent.patch | 227 + .../run-2/attempt-0/attempt.json | 155 + .../run-2/attempt-0/final.md | 28 + .../run-2/attempt-0/grade.json | 73 + .../run-3/attempt-0/agent.patch | 226 + .../run-3/attempt-0/attempt.json | 148 + .../run-3/attempt-0/final.md | 24 + .../run-3/attempt-0/grade.json | 73 + .../run-1/attempt-0/agent.patch | 255 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 23 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 282 + .../run-2/attempt-0/attempt.json | 155 + .../run-2/attempt-0/final.md | 27 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 292 + .../run-3/attempt-0/attempt.json | 161 + .../run-3/attempt-0/final.md | 23 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 289 + .../run-1/attempt-0/attempt.json | 148 + .../run-1/attempt-0/final.md | 33 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 308 + .../run-2/attempt-0/attempt.json | 155 + .../run-2/attempt-0/final.md | 23 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 285 + .../run-3/attempt-0/attempt.json | 148 + .../run-3/attempt-0/final.md | 24 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 184 + .../run-1/attempt-0/attempt.json | 209 + .../run-1/attempt-0/final.md | 28 + .../run-1/attempt-0/grade.json | 121 + .../run-2/attempt-0/agent.patch | 199 + .../run-2/attempt-0/attempt.json | 218 + .../run-2/attempt-0/final.md | 22 + .../run-2/attempt-0/grade.json | 121 + .../run-3/attempt-0/agent.patch | 188 + .../run-3/attempt-0/attempt.json | 203 + .../run-3/attempt-0/final.md | 17 + .../run-3/attempt-0/grade.json | 121 + .../final-summary-2026-10-01/core/REPORT.md | 169 + .../core/results.json | 3514 +++ .../final-summary-2026-10-01/hard/REPORT.md | 152 + .../hard/results.json | 2114 ++ .../final-triggers-2026-10-01/REPORT.md | 101 + .../inventory-skill.json | 209 + .../final-triggers-2026-10-01/meta.json | 277 + .../final-triggers-2026-10-01/runs.jsonl | 180 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 157 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 156 + .../run-2/attempt-0/attempt.json | 156 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 141 + .../run-2/attempt-0/attempt.json | 151 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 174 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 156 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 153 + .../run-2/attempt-0/attempt.json | 152 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 10 + .../run-2/attempt-0/attempt.json | 141 + .../run-1/attempt-0/attempt.json | 150 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 138 + .../run-1/attempt-0/final.md | 3 + .../run-2/attempt-0/attempt.json | 138 + .../run-2/attempt-0/final.md | 5 + .../run-1/attempt-0/attempt.json | 151 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 155 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 150 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 155 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 21 + .../run-2/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-2/attempt-0/final.md | 25 + .../run-1/attempt-0/attempt.json | 135 + .../run-1/attempt-0/final.md | 34 + .../run-2/attempt-0/attempt.json | 135 + .../run-2/attempt-0/final.md | 26 + .../run-1/attempt-0/attempt.json | 165 + .../run-2/attempt-0/attempt.json | 159 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 154 + .../run-1/attempt-0/attempt.json | 139 + .../run-1/attempt-0/final.md | 12 + .../run-2/attempt-0/attempt.json | 139 + .../run-2/attempt-0/final.md | 8 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-1/attempt-0/final.md | 27 + .../run-2/attempt-0/attempt.json | 137 + .../claude-opus-5-5/run-2/attempt-0/final.md | 26 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 14 + .../run-2/attempt-0/attempt.json | 136 + .../run-2/attempt-0/final.md | 18 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 137 + .../run-1/attempt-0/final.md | 4 + .../run-2/attempt-0/attempt.json | 137 + .../run-2/attempt-0/final.md | 7 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 145 + .../run-1/attempt-0/attempt.json | 159 + .../run-2/attempt-0/attempt.json | 141 + .../run-1/attempt-0/attempt.json | 139 + .../run-1/attempt-0/final.md | 8 + .../run-2/attempt-0/attempt.json | 139 + .../run-2/attempt-0/final.md | 8 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 141 + .../run-2/attempt-0/attempt.json | 138 + .../claude-opus-5-5/run-2/attempt-0/final.md | 17 + .../run-1/attempt-0/attempt.json | 143 + .../run-2/attempt-0/attempt.json | 142 + .../run-1/attempt-0/attempt.json | 143 + .../claude-fable-5-1/run-1/attempt-0/final.md | 17 + .../run-2/attempt-0/attempt.json | 143 + .../claude-fable-5-1/run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 143 + .../claude-opus-5-5/run-1/attempt-0/final.md | 18 + .../run-2/attempt-0/attempt.json | 143 + .../claude-opus-5-5/run-2/attempt-0/final.md | 17 + .../run-1/attempt-0/attempt.json | 143 + .../run-1/attempt-0/final.md | 6 + .../run-2/attempt-0/attempt.json | 143 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 133 + .../claude-fable-5-1/run-1/attempt-0/final.md | 12 + .../run-2/attempt-0/attempt.json | 133 + .../claude-fable-5-1/run-2/attempt-0/final.md | 16 + .../run-1/attempt-0/attempt.json | 133 + .../claude-opus-5-5/run-1/attempt-0/final.md | 29 + .../run-2/attempt-0/attempt.json | 133 + .../claude-opus-5-5/run-2/attempt-0/final.md | 27 + .../run-1/attempt-0/attempt.json | 133 + .../run-1/attempt-0/final.md | 25 + .../run-2/attempt-0/attempt.json | 133 + .../run-2/attempt-0/final.md | 31 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 141 + .../run-2/attempt-0/attempt.json | 140 + .../run-1/attempt-0/attempt.json | 136 + .../run-1/attempt-0/final.md | 5 + .../run-2/attempt-0/attempt.json | 136 + .../run-2/attempt-0/final.md | 5 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 152 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 146 + .../run-2/attempt-0/final.md | 19 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 149 + .../run-2/attempt-0/attempt.json | 149 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 151 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 155 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 148 + .../run-2/attempt-0/attempt.json | 152 + .../run-1/attempt-0/attempt.json | 155 + .../run-2/attempt-0/attempt.json | 148 + .../run-1/attempt-0/attempt.json | 140 + .../run-2/attempt-0/attempt.json | 149 + .../final-triggers-2026-10-01/summary.json | 2162 ++ .../results/selftest-final/selftest.json | 3257 +++ .../results/selftest-final/selftest.txt | 243 + .../skill-2026-09-30/inventory-skill.json | 448 + .../results/skill-2026-09-30/meta.json | 372 + .../results/skill-2026-09-30/runs.jsonl | 120 + .../run-1/attempt-0/agent.patch | 118 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 96 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 104 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 93 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 126 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 122 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 152 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 147 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 170 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 214 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 242 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 239 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 102 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 114 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 93 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 144 + .../run-1/attempt-0/attempt.json | 42 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/agent.patch | 135 + .../run-1/attempt-1/attempt.json | 40 + .../run-1/attempt-1/final.md | 1 + .../run-1/attempt-1/grade.json | 61 + .../run-2/attempt-0/agent.patch | 123 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 106 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 134 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 190 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 338 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 214 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 179 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 178 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 75 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 97 + .../run-2/attempt-0/agent.patch | 94 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 97 + .../run-3/attempt-0/agent.patch | 123 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 159 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 97 + .../run-2/attempt-0/agent.patch | 3668 +++ .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 97 + .../run-3/attempt-0/agent.patch | 152 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 97 + .../run-1/attempt-0/agent.patch | 109 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 65 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 82 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 87 + .../run-1/attempt-0/attempt.json | 45 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 118 + .../run-2/attempt-0/attempt.json | 45 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 154 + .../run-3/attempt-0/attempt.json | 45 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 111 + .../run-1/attempt-0/attempt.json | 46 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 98 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 61 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 38 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 85 + .../run-2/attempt-0/agent.patch | 39 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 85 + .../run-3/attempt-0/agent.patch | 73 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 85 + .../run-1/attempt-0/agent.patch | 47 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 43 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 47 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 0 .../run-1/attempt-0/attempt.json | 28 + .../run-1/attempt-1/agent.patch | 58 + .../run-1/attempt-1/attempt.json | 40 + .../run-1/attempt-1/final.md | 1 + .../run-1/attempt-1/grade.json | 61 + .../run-2/attempt-0/agent.patch | 48 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 56 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 60 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 61 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 66 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 79 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 78 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 84 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 81 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 55 + .../run-2/attempt-0/agent.patch | 85 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 55 + .../run-3/attempt-0/agent.patch | 77 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 93 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 55 + .../run-2/attempt-0/agent.patch | 92 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 55 + .../run-3/attempt-0/agent.patch | 95 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 55 + .../run-1/attempt-0/agent.patch | 209 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 126 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 136 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 172 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 142 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 160 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 110 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 86 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 115 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 121 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 120 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 122 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 123 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 95 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 72 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 115 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 76 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 118 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 182 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 77 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 81 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 170 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 139 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 128 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 121 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 74 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 139 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 130 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 61 + .../run-2/attempt-0/agent.patch | 197 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 61 + .../run-3/attempt-0/agent.patch | 226 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 61 + .../run-1/attempt-0/agent.patch | 93 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 81 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 58 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 113 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 67 + .../run-2/attempt-0/agent.patch | 100 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 67 + .../run-3/attempt-0/agent.patch | 78 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 67 + .../run-1/attempt-0/agent.patch | 860 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 382 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 530 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 503 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 345 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 492 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 642 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 489 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 702 + .../run-3/attempt-0/attempt.json | 32 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 409 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 396 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 515 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 166 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 295 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 589 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 221 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 180 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 242 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 582 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 412 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 937 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + .../run-1/attempt-0/agent.patch | 442 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/grade.json | 79 + .../run-2/attempt-0/agent.patch | 624 + .../run-2/attempt-0/attempt.json | 40 + .../run-2/attempt-0/final.md | 1 + .../run-2/attempt-0/grade.json | 79 + .../run-3/attempt-0/agent.patch | 472 + .../run-3/attempt-0/attempt.json | 40 + .../run-3/attempt-0/final.md | 1 + .../run-3/attempt-0/grade.json | 79 + evals/airflow/results/summary/REPORT.md | 137 + evals/airflow/results/summary/results.json | 3241 +++ .../results/triggers-2026-09-30/REPORT.md | 108 + .../triggers-2026-09-30/inventory-skill.json | 448 + .../results/triggers-2026-09-30/meta.json | 260 + .../results/triggers-2026-09-30/runs.jsonl | 120 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 35 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 24 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 24 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 34 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 36 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 24 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 34 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 24 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 34 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 26 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 15 + .../run-2/attempt-1/attempt.json | 24 + .../run-2/attempt-1/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 35 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 24 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 24 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 34 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 26 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 28 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 38 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 39 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 32 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 35 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 34 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 37 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../run-2/attempt-0/attempt.json | 37 + .../run-2/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 29 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 34 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 26 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 36 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 23 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 25 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 22 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 29 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 34 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 41 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 31 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 30 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 32 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 24 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 19 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 19 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 24 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 29 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 39 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 25 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 27 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 40 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-1/attempt.json | 38 + .../run-1/attempt-1/final.md | 1 + .../run-1/attempt-0/attempt.json | 28 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 35 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 38 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 26 + .../run-1/attempt-0/final.md | 1 + .../run-1/attempt-0/attempt.json | 37 + .../run-1/attempt-0/final.md | 1 + .../results/triggers-2026-09-30/summary.json | 1661 ++ .../triggers/fixtures/airflow2/README.md | 4 + .../fixtures/airflow2/dags/customer_sync.py | 19 + .../fixtures/airflow2/dags/partner_load.py | 17 + .../fixtures/airflow2/dags/weekly_report.py | 21 + .../airflow2/plugins/hooks/__init__.py | 0 .../airflow2/plugins/hooks/warehouse_hook.py | 18 + .../airflow2/plugins/operators/__init__.py | 0 .../plugins/operators/warehouse_operator.py | 15 + .../fixtures/airflow2/requirements.txt | 3 + .../fixtures/airflow2/tests/test_dags.py | 5 + .../triggers/fixtures/airflow3/README.md | 3 + .../fixtures/airflow3/dags/daily_revenue.py | 16 + .../fixtures/airflow3/dags/events_load.py | 15 + .../fixtures/airflow3/dags/orders_pipeline.py | 32 + .../fixtures/airflow3/requirements.txt | 3 + .../fixtures/airflow3/tests/data/orders.csv | 4 + .../fixtures/airflow3/tests/test_dags.py | 5 + evals/airflow/triggers/fixtures/dbt/README.md | 3 + .../triggers/fixtures/dbt/dbt_project.yml | 11 + .../fixtures/dbt/models/marts/orders.sql | 3 + .../fixtures/dbt/models/marts/schema.yml | 6 + .../fixtures/dbt/models/staging/_sources.yml | 7 + .../dbt/models/staging/stg_orders.sql | 6 + .../triggers/fixtures/python/README.md | 4 + .../triggers/fixtures/python/flows/etl.py | 14 + .../triggers/fixtures/python/pyproject.toml | 7 + .../fixtures/python/scripts/cleanup.sh | 3 + .../fixtures/python/scripts/nightly_report.py | 11 + .../fixtures/python/src/invoicer/__init__.py | 0 .../fixtures/python/src/invoicer/totals.py | 14 + .../fixtures/python/tests/test_smoke.py | 5 + evals/airflow/triggers/queries.json | 75 + evals/harness/README.md | 544 + evals/harness/claude_code.py | 456 + evals/harness/grading.py | 1476 ++ evals/harness/isolation.py | 406 + evals/harness/report.py | 463 + evals/harness/rescore_contamination.py | 528 + evals/harness/run_eval.py | 1823 ++ evals/harness/run_triggers.py | 711 + evals/harness/sanitize.py | 255 + evals/harness/setup_envs.sh | 78 + skills/airflow/_shared/airflow_check.py | 1272 + skills/airflow/_shared/dates-and-schedules.md | 237 + skills/airflow/_shared/manual_run.py | 241 + skills/airflow/_shared/replay_runs.py | 674 + .../airflow/authoring-airflow-dags/SKILL.md | 298 + .../references/dag-factories.md | 161 + .../references/dates-and-schedules.md | 237 + .../references/deferrable-and-sensors.md | 200 + .../scripts/airflow_check.py | 1272 + .../scripts/manual_run.py | 241 + .../scripts/replay_runs.py | 674 + .../airflow/migrating-to-airflow-3/SKILL.md | 299 + .../references/dates-and-schedules.md | 237 + .../references/runtime-traps.md | 224 + .../scripts/airflow_check.py | 1272 + .../scripts/compare_previews.py | 195 + .../scripts/manual_run.py | 241 + .../scripts/replay_compare.py | 364 + .../scripts/replay_runs.py | 674 + skills/airflow/testing-airflow-dags/SKILL.md | 295 + .../references/dates-and-schedules.md | 237 + .../references/test-patterns.md | 242 + .../scripts/airflow_check.py | 1272 + .../scripts/mutation_check.py | 250 + .../scripts/replay_runs.py | 674 + tests/evals/conftest.py | 9 + .../fixtures/claude_stream_context.jsonl | 3 + .../fixtures/claude_stream_skill_run.jsonl | 29 + tests/evals/test_claude_code.py | 571 + tests/evals/test_claude_code_integration.py | 70 + tests/evals/test_grading.py | 300 + tests/evals/test_grading_airflow.py | 162 + tests/evals/test_harness_hardening.py | 358 + tests/evals/test_isolation.py | 219 + tests/evals/test_report.py | 211 + tests/evals/test_rescore_contamination.py | 141 + tests/evals/test_run_eval.py | 393 + tests/evals/test_run_triggers.py | 149 + tests/evals/test_sanitize.py | 136 + tests/evals/test_token_cmd.py | 155 + .../airflow/fixtures/clean/clean_dag.py | 20 + .../env_isolation/parse_time_variable.py | 10 + .../fixtures/findings_only/poke_sensor_dag.py | 16 + .../fixtures/import_error/broken_dag.py | 8 + .../airflow/fixtures/import_error/good_dag.py | 11 + .../airflow/fixtures/schedules/intervals.py | 51 + .../fixtures/static/dag_defaults_neg.py | 25 + .../fixtures/static/dag_defaults_pos.py | 19 + .../airflow/fixtures/static/db_access_neg.py | 23 + .../airflow/fixtures/static/db_access_pos.py | 20 + .../fixtures/static/dynamic_dag_arg_neg.py | 32 + .../fixtures/static/dynamic_dag_arg_pos.py | 27 + .../fixtures/static/manual_run_dates_neg.py | 10 + .../fixtures/static/manual_run_dates_pos.py | 9 + .../fixtures/static/parse_time_helper_neg.py | 45 + .../fixtures/static/parse_time_helper_pos.py | 46 + .../static/removed_context_keys_neg.py | 28 + .../static/removed_context_keys_on_2x.py | 11 + .../static/removed_context_keys_pos.py | 30 + .../fixtures/static/removed_kwargs_neg.py | 24 + .../fixtures/static/removed_kwargs_on_2x.py | 15 + .../fixtures/static/removed_kwargs_pos.py | 25 + .../fixtures/static/reserved_param_neg.py | 27 + .../fixtures/static/reserved_param_pos.py | 25 + .../reserved_param_positional_3x_neg.py | 21 + .../static/reserved_param_positional_on_2x.py | 20 + .../fixtures/static/sdk_import_neg_2x.py | 22 + .../fixtures/static/sdk_import_neg_3x.py | 17 + .../airflow/fixtures/static/sdk_import_pos.py | 17 + .../airflow/fixtures/static/sensor_neg.py | 27 + .../airflow/fixtures/static/sensor_pos.py | 18 + .../skills/airflow/fixtures/static/sla_neg.py | 19 + .../skills/airflow/fixtures/static/sla_pos.py | 25 + .../fixtures/static/syntax_error_pos.py | 5 + .../fixtures/static/templates/removed_key.sql | 4 + .../fixtures/static/top_level_io_neg.py | 31 + .../fixtures/static/top_level_io_pos.py | 30 + .../airflow/fixtures/static/wall_clock_neg.py | 15 + .../airflow/fixtures/static/wall_clock_pos.py | 19 + .../airflow/fixtures/static/xcom_pull_neg.py | 21 + .../airflow/fixtures/static/xcom_pull_pos.py | 18 + tests/skills/airflow/test_airflow_check.py | 438 + tests/skills/airflow/test_compare_previews.py | 88 + tests/skills/airflow/test_manual_run.py | 90 + tests/skills/airflow/test_mutation_check.py | 144 + tests/skills/airflow/test_replay_compare.py | 110 + tests/skills/airflow/test_replay_runs.py | 204 + tests/skills/test_skill_spec.py | 198 + 3432 files changed, 382439 insertions(+), 8 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 evals/airflow/BENCHMARK.md create mode 100644 evals/airflow/cases/_example_smoke/case.yaml create mode 100644 evals/airflow/cases/_example_smoke/fixture/dags/orders_daily.py create mode 100644 evals/airflow/cases/_example_smoke/fixture/data/events.csv create mode 100644 evals/airflow/cases/_example_smoke/fixture/requirements.txt create mode 100644 evals/airflow/cases/_example_smoke/grade.py create mode 100644 evals/airflow/cases/_example_smoke/mutants/airflow2_imports/dags/event_counts.py create mode 100644 evals/airflow/cases/_example_smoke/reference/dags/event_counts.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/alt_reference/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/case.yaml create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/.gitignore create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/README.md create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/dags/sales_daily.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/inventory_movements.csv create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/reorder_points.csv create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/sales.csv create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/requirements.txt create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/grade.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/grader_data/inventory_movements.csv create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/airflow3_sdk_imports/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/catchup_omitted/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/excludes_run_date_movements/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/low_stock_at_or_below_point/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/standard_provider_import/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-airflow2-inventory-snapshot/reference/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/alt_reference/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/case.yaml create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/.gitignore create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/README.md create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/dags/marketing_spend_daily.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-27.csv.gz create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-28.csv.gz create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-29.csv.gz create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/marketing_spend.csv create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/fixture/requirements.txt create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/grade.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_json/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_records/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_via_variable/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/mutants/xcom_pull_without_task_ids/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-clickstream-claim-check/reference/dags/clickstream_page_stats.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/alt_reference/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/case.yaml create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/.gitignore create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/README.md create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/dags/store_dimension.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/orders.csv create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/stores.csv create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/requirements.txt create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/grade.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/cron_string_uses_ds/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/filters_on_data_interval_window/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/interval_timetable_ds_minus_one/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/wall_clock_yesterday/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-daily-revenue-yesterday/reference/dags/daily_revenue.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/alt_reference/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/case.yaml create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/.gitignore create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/README.md create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/dags/partner_registry_export.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-02/acme.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/acme.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/globex.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/initech.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/umbrella.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/globex.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/hooli.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partners.json create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/fixture/requirements.txt create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grade.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-02/globex.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/acme.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/hooli.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/initech.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/wayne.csv create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/mutants/missing_folder_crashes/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/mutants/parse_time_file_loop/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/mutants/registry_driven_mapping/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/mutants/single_task_loops_files/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/mutants/summary_default_trigger_rule/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-partner-feeds-mapping/reference/dags/partner_feeds.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/alt_reference/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/case.yaml create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/.gitignore create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/README.md create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/dags/warehouse_orders.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/incoming/shipments_2026-09-28.csv create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/orders_export.csv create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/requirements.txt create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/build_shipments_report.py create mode 100755 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/publish_shipments_report.sh create mode 100755 evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/refresh_orders_mart.sh create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/grade.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-28.csv create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-29.csv create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/airflow2_schedule_interval/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/append_only_load/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/bash_relative_script_path/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/poke_sensor_default_timeout/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/sensor_watches_landing_dir/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/mutants/toplevel_variable_get/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/authoring-vendor-shipments-etl/reference/dags/vendor_shipments.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/alt_reference/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/case.yaml create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_catalog_sync.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/fixture/data/partner_orders.csv create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/fixture/requirements.txt create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/grade.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/data_interval_fallback/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/local_tz_day/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/now_fallback/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/run_after_always/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/skip_manual_runs/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/start_date_fallback/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/mutants/template_left/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-manual-trigger-context/reference/dags/partner_feed.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/alt_reference/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/case.yaml create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/README.md create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/config/tables.json create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_maintenance.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/customers.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/inventory.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/order_items.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/orders.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/payments.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/products.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/refunds.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/shipments.csv create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/fixture/requirements.txt create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/grade.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/catalog_at_parse/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/description_date/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/hardcoded_tables/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/now_start_date/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/parallel_loads/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/set_order/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/mutants/utc_today_start_date/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-nondeterministic-parse/reference/dags/warehouse_ingest.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/alt_reference/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/case.yaml create mode 100644 evals/airflow/cases/debugging-variable-at-parse/fixture/README.md create mode 100644 evals/airflow/cases/debugging-variable-at-parse/fixture/dags/order_status_report.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/fixture/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/fixture/data/orders.csv create mode 100644 evals/airflow/cases/debugging-variable-at-parse/fixture/requirements.txt create mode 100644 evals/airflow/cases/debugging-variable-at-parse/grade.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/catchup_enabled/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/daily_preset/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/default_at_parse/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/hardcoded_path/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/schedule_dropped/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/mutants/variable_in_dag_body/dags/warehouse_load.py create mode 100644 evals/airflow/cases/debugging-variable-at-parse/reference/dags/warehouse_load.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/case.yaml create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/.gitignore create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/README.md create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/.airflowignore create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/__init__.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/hooks.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake_ingest.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/orders_diff.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/tools/catalog_stub.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/grade.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/defer_runner.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/eval_dags/_eval_lake_scenarios.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/lake_api.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/api_errors_polled_through/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/blocking_hook_kept/dags/lake/hooks.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/budget_restarts_each_try/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/cache_keyed_by_table/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/errors_as_success/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/finishes_from_triggerer/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/hardcoded_partition_files/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/timeout_retryable/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/hooks.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/alt_reference/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/case.yaml create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/.gitignore create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/README.md create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/.airflowignore create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partner_ingest.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/__init__.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/hooks.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/tools/manifest_stub.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/grade.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/defer_runner.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/eval_dags/_eval_manifest_scenarios.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/manifest_api.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_dag_run_start/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_in_xcom/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_this_try/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/api_error_airflow_exception/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/deferrable_flag_ignored/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/hardcoded_file_list/dags/partners/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/sync_hook_in_trigger/dags/partners/triggers.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/variable_anchor_non_string/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/hooks.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/sensors.py create mode 100644 evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/triggers.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_intake_hourly.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_reprocess.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_service_date_marts.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/denials_digest_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/provider_directory_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/hooks.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/alt_reference/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/case.yaml create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/.gitignore create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/README.md create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_intake_hourly.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_reprocess.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_service_date_marts.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/denials_digest_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/provider_directory_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/data/claims_export.csv create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/data/providers_export.csv create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/__init__.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/hooks.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/grade.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/hardsim.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_version_is_ds/dags/denials_digest_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_xcom_list_last/dags/denials_digest_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/fingerprint_prior_list_kept/dags/provider_directory_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/intake_bare_preset/dags/claims_intake_hourly.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_every_key_double_count/dags/claims_service_date_marts.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_rebuild_all_partitions/dags/claims_service_date_marts.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/recheck_log_jinja_or_undefined/dags/claims_reprocess.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/mutants/reprocess_wall_clock_cutoff/dags/claims_reprocess.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_intake_hourly.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_reprocess.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_service_date_marts.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/dags/denials_digest_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/dags/provider_directory_daily.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/hooks.py create mode 100644 evals/airflow/cases/hard-migration-claims-partitions/reference/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/close_report_weekly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/daily_close.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/fx_revaluation.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/gl_postings_hourly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/ledger_housekeeping.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/daily_close.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/fx_revaluation.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/vendor_payments.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/plugins/ledger_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/alt_reference/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-ledger-close/case.yaml create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/.gitignore create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/README.md create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/close_report_weekly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/daily_close.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/fx_revaluation.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/gl_postings_hourly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/ledger_housekeeping.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/daily_close.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/fx_revaluation.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/dags/vendor_payments.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/data/fx_rates.csv create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/data/postings.csv create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/__init__.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/workdays.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_plugin.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-ledger-close/grade.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/hardsim.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/audit_started_by_getattr/plugins/ledger_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_previous_calendar_day/dags/daily_close.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/daily_close.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/sql/daily_close.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/cursor_without_prior_dates/dags/gl_postings_hourly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/extract_interval_window/dags/gl_postings_hourly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/housekeeping_delta_trigger/dags/ledger_housekeeping.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/payments_utc_cron/dags/vendor_payments.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/mutants/weekly_prev_ds_minus_one_day/dags/close_report_weekly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/close_report_weekly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/daily_close.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/fx_revaluation.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/gl_postings_hourly.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/ledger_housekeeping.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/daily_close.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/fx_revaluation.sql create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/dags/vendor_payments.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/plugins/ledger_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-ledger-close/reference/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/_DELETE create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/billing_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/meter_registry.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/regional_readings.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/scripts/publish_invoices.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/tariffs_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/alt_reference/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/case.yaml create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/.gitignore create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/README.md create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/config/regions.json create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/billing_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/meter_registry.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/regional_readings.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/publish_invoices.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/sync_tariffs.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/tariffs_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_east.csv create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_west.csv create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/data/tariffs.csv create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/__init__.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/tariffs.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_plugin.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/grade.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/hardsim.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/billing_trigger_previous_calendar_day/dags/billing_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/manifest_rows_from_files/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/outlet_events_uri_key/plugins/meter_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/publish_conf_missing/dags/billing_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_all_landed_hours/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_run_after_hour/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_bare_cron/dags/tariffs_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_jinja_or_undefined/dags/scripts/sync_tariffs.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_trigger_latest_interval/dags/scripts/sync_tariffs.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/billing_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/meter_registry.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/regional_readings.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/publish_invoices.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/sync_tariffs.sh create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/tariffs_daily.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/dags/usage_rollup.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/assets.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/operators.py create mode 100644 evals/airflow/cases/hard-migration-meter-rollups/reference/requirements.txt create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/pools.json create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/case.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/README.md create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_attribution.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_carriers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_forecast.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_fraud_scores.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_orders.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_promotions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend_eu.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_warehouses.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_campaigns.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_cohorts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_forecast.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_prices.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_promotions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_signups.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_attribution.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_clicks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_fraud_scores.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory_us.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_ledger.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_payouts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_promotions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_ledger.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_hourly_rollup.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_v2.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_payouts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_prices.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_queries.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_hourly_rollup.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_spend.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_clicks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payouts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payroll.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_prices.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_promotions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_queries.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_settlements.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_shipments.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups_daily.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers_daily.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_campaigns.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_forecast.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_prices.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries_v2.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_refunds.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions_daily.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_settlements.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_suppliers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets_hourly_rollup.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_campaigns.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_carriers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat_eu.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_headcount.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions_us.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_refunds.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_spend.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_attribution.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_carriers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks_eu.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_csat.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_payouts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_prices.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_carriers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_cohorts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_fraud_scores.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_headcount.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders_daily.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_refunds.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend_eu.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_warehouses.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_attribution.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_campaigns.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_forecast.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_inventory.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_products.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_refunds.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_eu.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_suppliers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_attribution.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks_hourly_rollup.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_clicks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_cohorts.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_orders.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns_us.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions_us.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_spend.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_tickets.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_carriers.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_chargebacks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_clicks.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_backfill_safe.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_daily.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_v2.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_fraud_scores.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_inventory.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_payroll.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_sessions.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/registry.yaml create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/pools.json create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/fixture/requirements.txt create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/grade.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/dag_concurrency_not_pool/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/downstream_weight_rule/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/index_minute/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/minute_zero/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_parsing_context/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_validate_autoregister/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/now_doc_md/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/outer_try/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/pools.json create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/python_hash_minute/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/registry_per_pipeline_libyaml/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/set_owner/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/mutants/silent_skip/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/reference/dags/pipeline_factory.py create mode 100644 evals/airflow/cases/hard-reliability-factory-fleet/reference/pools.json create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/alt_reference/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/case.yaml create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/fixture/.gitignore create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/fixture/README.md create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/fixture/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/fixture/data/pageviews.jsonl create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/fixture/requirements.txt create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/grade.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/migsim.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/mutants/cleanup_relative_to_run_time/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/mutants/interval_vars_trigger_timetable/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/mutants/logical_date_bare_cron/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/mutants/ruff_fix_only/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-hourly-pageviews-templates/reference/dags/hourly_pageviews.py create mode 100644 evals/airflow/cases/migration-inventory-context/alt_reference/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/case.yaml create mode 100644 evals/airflow/cases/migration-inventory-context/fixture/.gitignore create mode 100644 evals/airflow/cases/migration-inventory-context/fixture/README.md create mode 100644 evals/airflow/cases/migration-inventory-context/fixture/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/fixture/data/stock_movements.csv create mode 100644 evals/airflow/cases/migration-inventory-context/fixture/requirements.txt create mode 100644 evals/airflow/cases/migration-inventory-context/grade.py create mode 100644 evals/airflow/cases/migration-inventory-context/migsim.py create mode 100644 evals/airflow/cases/migration-inventory-context/mutants/daily_cron_preset/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/mutants/prev_ds_default_none/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/mutants/timedelta_schedule_kept/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/mutants/xcom_pull_without_task_ids/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-inventory-context/reference/dags/inventory_snapshot.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/alt_reference/_DELETE create mode 100644 evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/daily_revenue.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/region_reference.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/alt_reference/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/case.yaml create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/README.md create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/dags/daily_revenue.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/dags/region_reference.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/dags/sql/daily_revenue.sql create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/data/orders.csv create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/requirements.txt create mode 100644 evals/airflow/cases/migration-revenue-previous-day/fixture/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/grade.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/migsim.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/mutants/data_interval_window/dags/sql/daily_revenue.sql create mode 100644 evals/airflow/cases/migration-revenue-previous-day/mutants/naive_cron_trigger/dags/daily_revenue.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/mutants/next_ds_left_in_sql/dags/sql/daily_revenue.sql create mode 100644 evals/airflow/cases/migration-revenue-previous-day/mutants/old_dagbag_test/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/reference/dags/daily_revenue.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/reference/dags/region_reference.py create mode 100644 evals/airflow/cases/migration-revenue-previous-day/reference/dags/sql/daily_revenue.sql create mode 100644 evals/airflow/cases/migration-revenue-previous-day/reference/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/migration-shipments-assets/alt_reference/dags/carrier_scorecard.py create mode 100644 evals/airflow/cases/migration-shipments-assets/alt_reference/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-shipments-assets/case.yaml create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/.gitignore create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/README.md create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/dags/carrier_scorecard.py create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/data/carrier_events.csv create mode 100644 evals/airflow/cases/migration-shipments-assets/fixture/requirements.txt create mode 100644 evals/airflow/cases/migration-shipments-assets/grade.py create mode 100644 evals/airflow/cases/migration-shipments-assets/migsim.py create mode 100644 evals/airflow/cases/migration-shipments-assets/mutants/db_session_kept/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-shipments-assets/mutants/watermark_current_run_only/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-shipments-assets/mutants/xcom_prior_dates_list/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-shipments-assets/mutants/xcom_pull_template_no_task_ids/dags/carrier_scorecard.py create mode 100644 evals/airflow/cases/migration-shipments-assets/reference/dags/carrier_scorecard.py create mode 100644 evals/airflow/cases/migration-shipments-assets/reference/dags/ingest_shipments.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/campaign_sync.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/case.yaml create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/README.md create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/dags/campaign_sync.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/data/campaign_events.csv create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/data/campaigns.csv create mode 100644 evals/airflow/cases/migration-weekly-catchup/fixture/requirements.txt create mode 100644 evals/airflow/cases/migration-weekly-catchup/grade.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/migsim.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/mutants/catchup_left_default/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/mutants/cron_trigger_current_week/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/mutants/deadline_from_logical_date/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/mutants/sla_kept_no_deadline/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/reference/dags/campaign_sync.py create mode 100644 evals/airflow/cases/migration-weekly-catchup/reference/dags/weekly_campaign_report.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/alt_reference/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/case.yaml create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/README.md create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/top_pages_weekly.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-01.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-02.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-03.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-04.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-05.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-06.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-07.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-08.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-09.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-10.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-11.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-12.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-13.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-14.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-15.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-16.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-17.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-18.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-19.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-20.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-21.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-22.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-23.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-24.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-25.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-26.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-27.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-28.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-29.csv create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/fixture/requirements.txt create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/grade.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/catchup_old_start_date/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/date_from_wall_clock/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/dynamic_start_date/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/max_active_tasks_only/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/no_concurrency_limit/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/same_day_skip_missing/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/skip_loaded_days/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/mutants/start_date_sep1_previous_day/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-backfill-pageviews/reference/dags/pageviews_daily.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/alt_reference/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/case.yaml create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/README.md create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/customers_snapshot.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/customers.csv create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/orders.csv create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/fixture/requirements.txt create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/grade.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/inclusive_interval_end/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/insert_missing_only/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/logical_date_minus_one/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/rollup_still_appends/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/skip_if_already_loaded/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/mutants/truncate_whole_table/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-idempotent-orders-load/reference/dags/orders_daily_load.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/alt_reference/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/case.yaml create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/README.md create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/desk_reference_refresh.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/desks.csv create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/fx_trades.csv create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/requirements.txt create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/grade.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/calendar_yesterday/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_interval_start/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_utc_start_date/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/utc_eleven_am_cron/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/wall_clock_run_day/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/scheduling-weekday-ny-business-day/reference/dags/fx_pnl_report.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/alt_reference/tests/test_shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/case.yaml create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/.gitignore create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/README.md create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/carrier_rates_weekly.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/carrier_rates.csv create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/shipments.csv create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/fixture/requirements.txt create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/grade.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/af3_import/dags/carrier_rates_weekly.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/missing_dependency/dags/shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/retries_dropped/dags/shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/undelivered_ignored/dags/shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/weekend_schedule/dags/shipments_daily.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/af3_test_apis/tests/test_dags.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/schedule_time_only/tests/test_dags.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/mutants/structure_only/tests/test_dags.py create mode 100644 evals/airflow/cases/testing-airflow2-ci-suite/reference/tests/test_dags.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/alt_reference/tests/test_csv_partition_operator.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/case.yaml create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/.gitignore create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/README.md create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/dags/.airflowignore create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/dags/load_raw_sales.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/__init__.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/hooks.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/operators.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/data/orders.csv create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/data/refunds.csv create mode 100644 evals/airflow/cases/testing-custom-operator-conn/fixture/requirements.txt create mode 100644 evals/airflow/cases/testing-custom-operator-conn/grade.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/conn_id_ignored/dags/warehouse/operators.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/hook_ignores_host/dags/warehouse/hooks.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/partition_filter_dropped/dags/warehouse/operators.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/rerun_duplicates/dags/warehouse/operators.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/schema_from_extra/dags/warehouse/hooks.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/conn_from_metadata_db/tests/test_warehouse.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/mocked_duckdb/tests/test_warehouse.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/conftest.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/test_warehouse.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/return_value_only/tests/test_warehouse.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/reference/tests/conftest.py create mode 100644 evals/airflow/cases/testing-custom-operator-conn/reference/tests/test_warehouse.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/alt_reference/tests/test_dags.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/case.yaml create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/.gitignore create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/README.md create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/dags/customer_snapshot.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/dags/marketing_spend.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/dags/orders_ingest.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/data/customers.csv create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/data/marketing_spend.csv create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/data/orders.csv create mode 100644 evals/airflow/cases/testing-dagbag-integrity/fixture/requirements.txt create mode 100644 evals/airflow/cases/testing-dagbag-integrity/grade.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/catchup_enabled/dags/customer_snapshot.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/dag_cycle/dags/orders_ingest.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/import_error/dags/customer_snapshot.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/missing_dependency/dags/orders_ingest.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/retries_disabled/dags/marketing_spend.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/airflow2_dagbag/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/default_args_retries_only/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/import_errors_only/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-dagbag-integrity/reference/tests/test_dag_integrity.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/alt_reference/tests/test_enrichment_pipeline.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/case.yaml create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/.gitignore create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/README.md create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/data/fx_rates.csv create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/data/orders.csv create mode 100644 evals/airflow/cases/testing-taskflow-transforms/fixture/requirements.txt create mode 100644 evals/airflow/cases/testing-taskflow-transforms/grade.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/dedupe_keeps_first/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/rounding_truncates/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/schedule_midnight/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/unknown_currency_as_usd/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/validation_bypassed/dags/orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/conftest.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/test_orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/conftest.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/test_orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/_DELETE create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/tests/test_dag_sources.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/conftest.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/test_orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/conftest.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/test_orders_enrichment.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/reference/tests/conftest.py create mode 100644 evals/airflow/cases/testing-taskflow-transforms/reference/tests/test_orders_enrichment.py create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/REPORT.md create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/REVIEW.md create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/all/REPORT.md create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/all/results.json create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/contamination.jsonl create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/REPORT.md create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/results.json create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/REPORT.md create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/results.json create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/meta.json create mode 100644 evals/airflow/results/altimate-code-2026-09-30-summary/results.json create mode 100644 evals/airflow/results/baseline-2026-09-30/inventory-baseline.json create mode 100644 evals/airflow/results/baseline-2026-09-30/meta.json create mode 100644 evals/airflow/results/baseline-2026-09-30/report/REPORT.md create mode 100644 evals/airflow/results/baseline-2026-09-30/report/results.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs.jsonl create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-2026-09-30/selftest/selftest.json create mode 100644 evals/airflow/results/baseline-n3-2026-09-30/REPORT.md create mode 100644 evals/airflow/results/baseline-n3-2026-09-30/results.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/inventory-baseline.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/meta.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs.jsonl create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-1/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-1/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-1/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-1/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-campaign-2026-10-01.state create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/meta.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-fable-5-1/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-sonnet-5-5/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-01/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-02/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-03/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-04/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-05/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-01/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-02/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-03/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-03/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/migrate-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dagster/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dagster/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-databricks-job/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-debug/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-debug/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-debug/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-model/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-model/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-model/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-test/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-test/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-dbt-test/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-gha/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-gha/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-gha/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-prefect/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-prefect/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-pytest/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-pytest/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-pytest/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-pytest/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-wrong-day-script/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-wrong-day-script/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-01/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-01/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/cc-triggers-2026-10-01/runs/test-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-campaign-2026-10-01.state create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-baseline-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-inventory-context/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-shipments-assets/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/migration-weekly-catchup/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-opus-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-airflow2-inventory-snapshot/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-clickstream-claim-check/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-daily-revenue-yesterday/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-partner-feeds-mapping/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/authoring-vendor-shipments-etl/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-manual-trigger-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-nondeterministic-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/debugging-variable-at-parse/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-hourly-pageviews-templates/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-inventory-context/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-revenue-previous-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-shipments-assets/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/migration-weekly-catchup/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-backfill-pageviews/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-idempotent-orders-load/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/scheduling-weekday-ny-business-day/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-airflow2-ci-suite/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-custom-operator-conn/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-dagbag-integrity/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-core-skill-claude-sonnet-5-5-2026-10-01/runs/testing-taskflow-transforms/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/inventory-baseline.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-3/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-3/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-1/attempt-3/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-3/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-3/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-1/attempt-3/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-3/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-3/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-1/attempt-3/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-claims-partitions/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-2/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-2/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-2/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-3/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-3/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-3/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-1/attempt-3/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-ledger-close/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-migration-meter-rollups/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-fable-5-1-2026-10-01/runs/hard-reliability-factory-fleet/claude-fable-5-1/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-opus-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-debug-partition-sensor/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-deferrable-sensor-retry-budget/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-claims-partitions/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-ledger-close/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-migration-meter-rollups/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01/runs/hard-reliability-factory-fleet/claude-sonnet-5-5/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/final-summary-2026-10-01/core/REPORT.md create mode 100644 evals/airflow/results/final-summary-2026-10-01/core/results.json create mode 100644 evals/airflow/results/final-summary-2026-10-01/hard/REPORT.md create mode 100644 evals/airflow/results/final-summary-2026-10-01/hard/results.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/REPORT.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/inventory-skill.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/meta.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs.jsonl create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-01/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-02/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-03/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-04/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-05/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/author-06/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-01/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-02/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-03/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-04/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-05/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/migrate-06/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-cron/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dagster/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-databricks-job/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-debug/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-model/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-dbt-test/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-gha/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-prefect/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-pytest/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-snowflake/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-fable-5-1/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-opus-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-what-dag/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/near-wrong-day-script/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-01/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-02/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-03/claude-sonnet-5-5/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-04/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-05/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-fable-5-1/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-fable-5-1/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-opus-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-opus-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-sonnet-5-5/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/runs/test-06/claude-sonnet-5-5/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/final-triggers-2026-10-01/summary.json create mode 100644 evals/airflow/results/selftest-final/selftest.json create mode 100644 evals/airflow/results/selftest-final/selftest.txt create mode 100644 evals/airflow/results/skill-2026-09-30/inventory-skill.json create mode 100644 evals/airflow/results/skill-2026-09-30/meta.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs.jsonl create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-airflow2-ci-suite/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-dagbag-integrity/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/agent.patch create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/attempt.json create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/final.md create mode 100644 evals/airflow/results/skill-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0/grade.json create mode 100644 evals/airflow/results/summary/REPORT.md create mode 100644 evals/airflow/results/summary/results.json create mode 100644 evals/airflow/results/triggers-2026-09-30/REPORT.md create mode 100644 evals/airflow/results/triggers-2026-09-30/inventory-skill.json create mode 100644 evals/airflow/results/triggers-2026-09-30/meta.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs.jsonl create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/author-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/migrate-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-cron/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-cron/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-cron/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-cron/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dagster/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dagster/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dagster/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dagster/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-databricks-job/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-databricks-job/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-databricks-job/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-databricks-job/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-debug/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-debug/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-debug/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-debug/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-model/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-model/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-model/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-model/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-test/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-test/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-test/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-dbt-test/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-gha/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-gha/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-gha/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-gha/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-prefect/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-prefect/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-prefect/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-prefect/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-pytest/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-pytest/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-pytest/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-pytest/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-snowflake/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-snowflake/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-snowflake/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-snowflake/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-what-dag/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-what-dag/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-what-dag/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-what-dag/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-wrong-day-script/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-wrong-day-script/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-wrong-day-script/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/near-wrong-day-script/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-01/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-01/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-02/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-02/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-03/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-04/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-04/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-05/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-05/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-06/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json create mode 100644 evals/airflow/results/triggers-2026-09-30/runs/test-06/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md create mode 100644 evals/airflow/results/triggers-2026-09-30/summary.json create mode 100644 evals/airflow/triggers/fixtures/airflow2/README.md create mode 100644 evals/airflow/triggers/fixtures/airflow2/dags/customer_sync.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/dags/partner_load.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/dags/weekly_report.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/plugins/hooks/__init__.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/plugins/hooks/warehouse_hook.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/plugins/operators/__init__.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/plugins/operators/warehouse_operator.py create mode 100644 evals/airflow/triggers/fixtures/airflow2/requirements.txt create mode 100644 evals/airflow/triggers/fixtures/airflow2/tests/test_dags.py create mode 100644 evals/airflow/triggers/fixtures/airflow3/README.md create mode 100644 evals/airflow/triggers/fixtures/airflow3/dags/daily_revenue.py create mode 100644 evals/airflow/triggers/fixtures/airflow3/dags/events_load.py create mode 100644 evals/airflow/triggers/fixtures/airflow3/dags/orders_pipeline.py create mode 100644 evals/airflow/triggers/fixtures/airflow3/requirements.txt create mode 100644 evals/airflow/triggers/fixtures/airflow3/tests/data/orders.csv create mode 100644 evals/airflow/triggers/fixtures/airflow3/tests/test_dags.py create mode 100644 evals/airflow/triggers/fixtures/dbt/README.md create mode 100644 evals/airflow/triggers/fixtures/dbt/dbt_project.yml create mode 100644 evals/airflow/triggers/fixtures/dbt/models/marts/orders.sql create mode 100644 evals/airflow/triggers/fixtures/dbt/models/marts/schema.yml create mode 100644 evals/airflow/triggers/fixtures/dbt/models/staging/_sources.yml create mode 100644 evals/airflow/triggers/fixtures/dbt/models/staging/stg_orders.sql create mode 100644 evals/airflow/triggers/fixtures/python/README.md create mode 100644 evals/airflow/triggers/fixtures/python/flows/etl.py create mode 100644 evals/airflow/triggers/fixtures/python/pyproject.toml create mode 100755 evals/airflow/triggers/fixtures/python/scripts/cleanup.sh create mode 100644 evals/airflow/triggers/fixtures/python/scripts/nightly_report.py create mode 100644 evals/airflow/triggers/fixtures/python/src/invoicer/__init__.py create mode 100644 evals/airflow/triggers/fixtures/python/src/invoicer/totals.py create mode 100644 evals/airflow/triggers/fixtures/python/tests/test_smoke.py create mode 100644 evals/airflow/triggers/queries.json create mode 100644 evals/harness/README.md create mode 100644 evals/harness/claude_code.py create mode 100644 evals/harness/grading.py create mode 100644 evals/harness/isolation.py create mode 100644 evals/harness/report.py create mode 100644 evals/harness/rescore_contamination.py create mode 100644 evals/harness/run_eval.py create mode 100644 evals/harness/run_triggers.py create mode 100644 evals/harness/sanitize.py create mode 100755 evals/harness/setup_envs.sh create mode 100644 skills/airflow/_shared/airflow_check.py create mode 100644 skills/airflow/_shared/dates-and-schedules.md create mode 100644 skills/airflow/_shared/manual_run.py create mode 100644 skills/airflow/_shared/replay_runs.py create mode 100644 skills/airflow/authoring-airflow-dags/SKILL.md create mode 100644 skills/airflow/authoring-airflow-dags/references/dag-factories.md create mode 100644 skills/airflow/authoring-airflow-dags/references/dates-and-schedules.md create mode 100644 skills/airflow/authoring-airflow-dags/references/deferrable-and-sensors.md create mode 100644 skills/airflow/authoring-airflow-dags/scripts/airflow_check.py create mode 100644 skills/airflow/authoring-airflow-dags/scripts/manual_run.py create mode 100644 skills/airflow/authoring-airflow-dags/scripts/replay_runs.py create mode 100644 skills/airflow/migrating-to-airflow-3/SKILL.md create mode 100644 skills/airflow/migrating-to-airflow-3/references/dates-and-schedules.md create mode 100644 skills/airflow/migrating-to-airflow-3/references/runtime-traps.md create mode 100644 skills/airflow/migrating-to-airflow-3/scripts/airflow_check.py create mode 100644 skills/airflow/migrating-to-airflow-3/scripts/compare_previews.py create mode 100644 skills/airflow/migrating-to-airflow-3/scripts/manual_run.py create mode 100644 skills/airflow/migrating-to-airflow-3/scripts/replay_compare.py create mode 100644 skills/airflow/migrating-to-airflow-3/scripts/replay_runs.py create mode 100644 skills/airflow/testing-airflow-dags/SKILL.md create mode 100644 skills/airflow/testing-airflow-dags/references/dates-and-schedules.md create mode 100644 skills/airflow/testing-airflow-dags/references/test-patterns.md create mode 100644 skills/airflow/testing-airflow-dags/scripts/airflow_check.py create mode 100644 skills/airflow/testing-airflow-dags/scripts/mutation_check.py create mode 100644 skills/airflow/testing-airflow-dags/scripts/replay_runs.py create mode 100644 tests/evals/conftest.py create mode 100644 tests/evals/fixtures/claude_stream_context.jsonl create mode 100644 tests/evals/fixtures/claude_stream_skill_run.jsonl create mode 100644 tests/evals/test_claude_code.py create mode 100644 tests/evals/test_claude_code_integration.py create mode 100644 tests/evals/test_grading.py create mode 100644 tests/evals/test_grading_airflow.py create mode 100644 tests/evals/test_harness_hardening.py create mode 100644 tests/evals/test_isolation.py create mode 100644 tests/evals/test_report.py create mode 100644 tests/evals/test_rescore_contamination.py create mode 100644 tests/evals/test_run_eval.py create mode 100644 tests/evals/test_run_triggers.py create mode 100644 tests/evals/test_sanitize.py create mode 100644 tests/evals/test_token_cmd.py create mode 100644 tests/skills/airflow/fixtures/clean/clean_dag.py create mode 100644 tests/skills/airflow/fixtures/env_isolation/parse_time_variable.py create mode 100644 tests/skills/airflow/fixtures/findings_only/poke_sensor_dag.py create mode 100644 tests/skills/airflow/fixtures/import_error/broken_dag.py create mode 100644 tests/skills/airflow/fixtures/import_error/good_dag.py create mode 100644 tests/skills/airflow/fixtures/schedules/intervals.py create mode 100644 tests/skills/airflow/fixtures/static/dag_defaults_neg.py create mode 100644 tests/skills/airflow/fixtures/static/dag_defaults_pos.py create mode 100644 tests/skills/airflow/fixtures/static/db_access_neg.py create mode 100644 tests/skills/airflow/fixtures/static/db_access_pos.py create mode 100644 tests/skills/airflow/fixtures/static/dynamic_dag_arg_neg.py create mode 100644 tests/skills/airflow/fixtures/static/dynamic_dag_arg_pos.py create mode 100644 tests/skills/airflow/fixtures/static/manual_run_dates_neg.py create mode 100644 tests/skills/airflow/fixtures/static/manual_run_dates_pos.py create mode 100644 tests/skills/airflow/fixtures/static/parse_time_helper_neg.py create mode 100644 tests/skills/airflow/fixtures/static/parse_time_helper_pos.py create mode 100644 tests/skills/airflow/fixtures/static/removed_context_keys_neg.py create mode 100644 tests/skills/airflow/fixtures/static/removed_context_keys_on_2x.py create mode 100644 tests/skills/airflow/fixtures/static/removed_context_keys_pos.py create mode 100644 tests/skills/airflow/fixtures/static/removed_kwargs_neg.py create mode 100644 tests/skills/airflow/fixtures/static/removed_kwargs_on_2x.py create mode 100644 tests/skills/airflow/fixtures/static/removed_kwargs_pos.py create mode 100644 tests/skills/airflow/fixtures/static/reserved_param_neg.py create mode 100644 tests/skills/airflow/fixtures/static/reserved_param_pos.py create mode 100644 tests/skills/airflow/fixtures/static/reserved_param_positional_3x_neg.py create mode 100644 tests/skills/airflow/fixtures/static/reserved_param_positional_on_2x.py create mode 100644 tests/skills/airflow/fixtures/static/sdk_import_neg_2x.py create mode 100644 tests/skills/airflow/fixtures/static/sdk_import_neg_3x.py create mode 100644 tests/skills/airflow/fixtures/static/sdk_import_pos.py create mode 100644 tests/skills/airflow/fixtures/static/sensor_neg.py create mode 100644 tests/skills/airflow/fixtures/static/sensor_pos.py create mode 100644 tests/skills/airflow/fixtures/static/sla_neg.py create mode 100644 tests/skills/airflow/fixtures/static/sla_pos.py create mode 100644 tests/skills/airflow/fixtures/static/syntax_error_pos.py create mode 100644 tests/skills/airflow/fixtures/static/templates/removed_key.sql create mode 100644 tests/skills/airflow/fixtures/static/top_level_io_neg.py create mode 100644 tests/skills/airflow/fixtures/static/top_level_io_pos.py create mode 100644 tests/skills/airflow/fixtures/static/wall_clock_neg.py create mode 100644 tests/skills/airflow/fixtures/static/wall_clock_pos.py create mode 100644 tests/skills/airflow/fixtures/static/xcom_pull_neg.py create mode 100644 tests/skills/airflow/fixtures/static/xcom_pull_pos.py create mode 100644 tests/skills/airflow/test_airflow_check.py create mode 100644 tests/skills/airflow/test_compare_previews.py create mode 100644 tests/skills/airflow/test_manual_run.py create mode 100644 tests/skills/airflow/test_mutation_check.py create mode 100644 tests/skills/airflow/test_replay_compare.py create mode 100644 tests/skills/airflow/test_replay_runs.py create mode 100644 tests/skills/test_skill_spec.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e7a8fdb..e770a0a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -44,6 +44,17 @@ "./skills/databricks/optimizing-databricks-sql" ] }, + { + "name": "airflow-skills", + "description": "Skills for Apache Airflow 2.x/3.x: authoring and fixing DAGs, testing DAGs, and migrating to Airflow 3", + "source": "./", + "strict": false, + "skills": [ + "./skills/airflow/authoring-airflow-dags", + "./skills/airflow/migrating-to-airflow-3", + "./skills/airflow/testing-airflow-dags" + ] + }, { "name": "altimate-code", "description": "Delegate data engineering tasks to altimate-code — a specialized CLI agent with 100+ data tools (warehouse profiling, column lineage, dbt build/test, FinOps)", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..79920af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Eval fixtures and grader data are byte-exact inputs; never normalise line endings. +evals/airflow/cases/** -text +evals/airflow/triggers/fixtures/** -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..740586a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "0 5 * * *" # nightly full grader self-test + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Skill spec lint + JSON + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install lint deps + run: python -m pip install --quiet pyyaml pytest + - name: Validate JSON manifests + run: | + for f in .claude-plugin/marketplace.json registry.json; do + python -m json.tool "$f" > /dev/null && echo "ok $f" + done + - name: Skill spec tests + run: python -m pytest -q tests/skills/test_skill_spec.py + + airflow-tests: + name: Airflow scripts, harness tests, grader self-test + runs-on: ubuntu-latest + needs: lint + timeout-minutes: 45 + env: + EVAL_ENV_ROOT: ${{ github.workspace }}/../des-evals + EVAL_WORK_ROOT: ${{ github.workspace }}/../des-evals-work + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: evals/harness/setup_envs.sh + # Same pinned envs as local evals: apache-airflow 3.3.2 and 2.11.2 with the + # official constraints files. uv's download cache makes this a fast re-link. + - name: Create Airflow envs + run: evals/harness/setup_envs.sh 3.3 2.11 + - name: Airflow skill script tests + harness tests + # Env-dependent tests skip when a venv is missing; fail instead of skipping here. + run: | + test -x "$EVAL_ENV_ROOT/airflow-3.3/bin/python" && test -x "$EVAL_ENV_ROOT/airflow-2.11/bin/python" + "$EVAL_ENV_ROOT/airflow-3.3/bin/python" -m pytest -q -rs tests/skills/airflow tests/evals + # The full self-test takes ~3.5 CPU-hours (160 variants), so PRs run a fast + # subset covering both envs; the grader-self-test job below runs everything. + - name: Grader self-test, quick subset (no LLM) + run: | + "$EVAL_ENV_ROOT/airflow-3.3/bin/python" evals/harness/run_eval.py \ + --cases evals/airflow/cases --self-test --split all --parallel 4 \ + --case authoring-airflow2-inventory-snapshot \ + --case authoring-daily-revenue-yesterday \ + --case testing-airflow2-ci-suite + + grader-self-test: + name: Full grader self-test (no LLM) + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + needs: airflow-tests + timeout-minutes: 180 + env: + EVAL_ENV_ROOT: ${{ github.workspace }}/../des-evals + EVAL_WORK_ROOT: ${{ github.workspace }}/../des-evals-work + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: evals/harness/setup_envs.sh + - name: Create Airflow envs + run: evals/harness/setup_envs.sh 3.3 2.11 + - name: Grader self-test, all cases + run: | + "$EVAL_ENV_ROOT/airflow-3.3/bin/python" evals/harness/run_eval.py \ + --cases evals/airflow/cases --self-test --split all --parallel 4 diff --git a/.gitignore b/.gitignore index 2a22d43..11a80c6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ logs/ # Test outputs .pytest_cache/ +.ruff_cache/ __pycache__/ *.pyc @@ -42,3 +43,23 @@ node_modules/ .local/ tmp/ temp/ + +# Eval results: commit summaries (meta.json, runs.jsonl, inventory-*.json, +# report/, selftest/) and small per-attempt files (grade.json, attempt.json, +# agent.patch, final.md). Raw agent transcripts and logs stay local. +evals/**/results/*/runs/**/events.jsonl +evals/**/results/*/runs/**/stderr.log +evals/**/results/*/runs/**/grade.log +evals/**/results/*/runs/**/trace.json +evals/**/results/*/runs/**/workspace/ +# Harness smoke runs (case ids starting with "_") +evals/**/results/_*/ +# Harness crash dumps and backups (runs.jsonl.prev-, *.harness-exception, editor/merge leftovers) +evals/**/results/**/*.harness-exception +evals/**/results/**/*.prev-* +evals/**/results/**/*.bak +evals/**/results/**/*.orig +evals/**/results/**/*.rej +evals/**/results/**/*.tmp +evals/**/results/**/core.[0-9]* +evals/**/results/**/sandbox.sb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac9ad56..da86cee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,15 +17,17 @@ Thanks for your interest in contributing! This document outlines how to get invo ## Skill Structure -Skills are markdown files with YAML frontmatter: +Skills are markdown files with YAML frontmatter. Use only the fields defined by the [Agent Skills spec](https://agentskills.io/specification) so skills stay portable across Claude Code, claude.ai, the API and other agents (unknown keys such as `triggers:` are ignored by Claude Code and rejected by stricter loaders): ```markdown --- -name: skill-name -description: | - When to use this skill -triggers: - - keyword pattern +name: skill-name # required; must equal the directory name; lowercase, digits, hyphens; <= 64 chars +description: >- # required; <= 1024 chars; this is the only text used to decide when to load the skill + What the skill does and when to use it. Not for ... (use other-skill). +license: MIT # optional +compatibility: Requires ... # optional; runtime requirements, <= 500 chars +metadata: # optional; string keys and string values + version: "0.1.0" --- # Skill Title @@ -33,15 +35,110 @@ triggers: Instructions for Claude... ``` +Quote or block-scalar (`>-`, `|`) any description that contains `: `; the frontmatter must parse as strict YAML. + Place new skills in the appropriate directory: - `skills/dbt/` - dbt-related skills - `skills/snowflake/` - Snowflake-related skills +- `skills/airflow/` - Apache Airflow skills - Create a new directory for other tools +## Skill Structure and Evals + +New skills follow the layout used by `skills/airflow/`: + +``` +skills/// +├── SKILL.md # workflow + gotchas; target < 300 lines (hard cap 500) +├── references/ # detail loaded on demand +└── scripts/ # deterministic checks the agent runs +``` + +- **SKILL.md** holds the workflow, defaults and gotchas. Every line should fix a failure observed in baseline runs; explain the why. +- **references/** files are linked from SKILL.md with an explicit read-when condition (e.g. "read `references/x.md` only if the project is on Airflow 2.x"). +- **scripts/** are stdlib-only Python that print JSON to stdout and use exit codes, so they run in any project environment. +- **Shared files** (e.g. `skills/airflow/_shared/`) are vendored: each skill that uses one carries a byte-identical copy, because an installed skill can only read its own directory. Edit the canonical file, then re-copy it; `tests/skills/test_skill_spec.py` fails on any drift. `_shared` is never listed as a skill. + +### Descriptions + +- State what the skill does, then the triggers: realistic user phrasings and symptoms (error messages, "it processes the wrong day"). +- End with `Not for ...` boundaries that name the neighbouring skill to use instead. +- Do not summarise the workflow in the description; the agent may act on the summary and skip the body. + +### Evals + +Skills that change agent behaviour need evals. Add cases under `evals//cases//`: + +- `case.yaml` - prompt, target version, split, limits +- `fixture/` - the project the agent starts from +- `reference/` (and optional `alt_reference/`) - a correct solution that must pass +- `mutants/` - plausible wrong solutions, each of which must fail at least one primary check +- `grade.py` - runs the agent's code and writes primary (behaviour) and secondary (process/style) checks + +Before opening a PR: + +1. Run the grader self-test (no LLM): `python evals/harness/run_eval.py --cases evals//cases --self-test --split all` +2. Run the baseline and skill arms and compare them with `evals/harness/report.py`. See [evals/harness/README.md](evals/harness/README.md). +3. Run `pytest tests/` (spec lint, script tests, harness tests). CI runs the same checks. + +### Airflow evals + +The Airflow skills, cases and harness are the reference implementation of the above. Scope: changes +to `skills/airflow/`, `evals/airflow/` and `evals/harness/`. The harness itself is Airflow-specific +today (pinned venvs, grading helpers, trigger queries, macOS `sandbox-exec` isolation); another tool +needs its own cases and graders, and can reuse the run, report and sanitize logic. + +**Environments.** `evals/harness/setup_envs.sh` (needs `uv`) creates four pinned venvs under +`${EVAL_ENV_ROOT:-~/.cache/des-evals}`: grader envs `airflow-3.3` (Airflow 3.3.2) and `airflow-2.11` +(Airflow 2.11.2), and separate agent envs `agent-airflow-3.3` / `agent-airflow-2.11` that the agent +works in. Graders never run in an agent env. Run the harness and the tests with the 3.3 grader +interpreter, which has pyyaml and pytest: + +```bash +evals/harness/setup_envs.sh +PY=~/.cache/des-evals/airflow-3.3/bin/python +$PY evals/harness/run_eval.py --cases evals/airflow/cases --self-test --split all # every case, no LLM +$PY -m pytest tests/ -m "not slow" +``` + +`--split all` matters: without it the self-test only covers the dev split, and holdout graders go +unchecked. Add `--parallel 4` to speed it up. The self-test runs every case's reference and +`alt_reference` (must pass), the untouched fixture and every mutant (must fail at least one primary +check). Save its output with `--out evals/airflow/results/selftest-final` after changing a grader. + +**Runners and auth.** +- `--runner claude-code`: Claude Code on a subscription. Each attempt has a fresh config dir, so a + logged-in `claude` is not used as is. Set `CLAUDE_CODE_OAUTH_TOKEN` (one token for the campaign, + e.g. from `claude setup-token`) or `EVAL_CLAUDE_TOKEN_CMD`, a command that prints a current token + (for example the token of your logged-in Claude Code, refreshed near expiry). The command runs + before every attempt, so long campaigns survive token expiry. Never commit or log a token; + `sanitize.py` redacts `sk-ant-*` tokens and every fetched token in result files. +- `--runner altimate-code` (default): uses the provider auth of your altimate-code install + (`~/.local/share/altimate-code/auth.json`, plus `GOOGLE_APPLICATION_CREDENTIALS`, + `GOOGLE_CLOUD_PROJECT` and `VERTEX_LOCATION` for Vertex models). + +**Splits.** The skills are iterated on `dev` cases only. Do not read holdout prompts, fixtures or +graders while writing or changing a skill; report holdout and dev (tuned-on) separately. + +**Hard cases** (`suite: hard`, ids `hard-*`) must be: +- **discriminating:** a current frontier model without the skills fails at least one of two probe + runs for a genuine reason (read the transcript; a flaky grader, a killed run or an ambiguous + prompt does not count). Cases the baseline solves are dropped, not made obscure. +- **probed and reviewed:** probe with at least two baseline runs, read the transcripts, and have a + second reviewer (a person or another model) read the prompt, graders and mutants for fairness before + the case is kept. +- **fair:** every primary check follows from the prompt and fixture as a senior engineer would read + them. Each case ships a reference and an independently written `alt_reference` that pass, and + mutants that fail, and the self-test must be clean. If a valid alternative solution fails a + check, fix the check or state the requirement in the prompt (what, not how). + +Selecting cases on one model's baseline failures biases that model's baseline downward; say which +model was used for selection when you report results. + ## Submitting Changes 1. Make your changes in a feature branch -2. Test your skill with Claude Code +2. Test your skill with Claude Code and run the checks in [Skill Structure and Evals](#skill-structure-and-evals) 3. Submit a pull request with: - Clear description of changes - Any testing you performed diff --git a/README.md b/README.md index e6d7281..efae765 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Altimate Data Skills is a collection of Claude Code skills that encode the workf - **3x improvement** on model creation tasks vs baseline - **84% pass rate** on Snowflake query optimization (62 TPC-H queries, 1TB dataset) - **3.6x better performance** gains vs baseline (16.8% avg improvement vs 4.7%) +- **Airflow skills** (Claude Code, Sonnet 5.5 and Opus 5.5, 6 hard whole-project tasks, 3 runs each): on the 2 holdout cases pass rates went from 67% to 100% (Sonnet 5.5) and from 83% to 100% (Opus 5.5). Small sample and wide CIs; on common Airflow tasks (20-case core suite) both models are already near ceiling and the skills change little. See [Airflow Skills benchmark](#airflow-skills-1) - Skills that teach Claude *how* to work, not just *what* to write ## Quick Start @@ -29,6 +30,9 @@ Install individual skill packs: # Install Snowflake skills /plugin install snowflake-skills@data-engineering-skills +# Install Airflow skills +/plugin install airflow-skills@data-engineering-skills + # Install altimate-code delegation skill /plugin install altimate-code@data-engineering-skills ``` @@ -55,6 +59,16 @@ Install individual skill packs: | **optimizing-query-by-id** | Performance tuning | Optimize using query ID from history | | **optimizing-query-text** | Performance tuning | Profile query → Identify bottlenecks → Apply patterns | +### Airflow Skills + +For Apache Airflow 2.x and 3.x projects. Each skill detects the project's Airflow version first and ships stdlib-only helper scripts (JSON output) that the agent runs to verify its work. + +| Skill | Purpose | Key Behaviors | +|-------|---------|---------------| +| **authoring-airflow-dags** | Write, change and fix DAGs, including deferrable sensors and config-driven DAG factories | Detect version → Read existing DAGs → Write the period contract → Verify until clean → Execute with `airflow dags test`, then replay several runs (`replay_runs.py`) when state crosses runs. References: deferrable sensors and retry budgets, DAG factories | +| **migrating-to-airflow-3** | Port 2.x DAGs to 3.x | Inventory → Mechanical fixes → BEFORE/AFTER schedule preview must match → Fix runtime-only traps (`xcom_pull`, templates, manual-trigger intervals) → Replay BEFORE vs AFTER outputs on both versions (`replay_compare.py`) → Migration report | +| **testing-airflow-dags** | pytest suites for DAGs and custom operators | Offline `conftest.py` (no scheduler/connections) → Test each rule → Mutation check proves the suite catches bugs (`mutation_check.py`) → Replay multi-run state | + ### altimate-code Delegation | Skill | Purpose | Key Behaviors | @@ -110,6 +124,9 @@ Skills activate automatically based on your request: | "Document the revenue metrics" | `documenting-dbt-models` | | "Create an incremental model for events" | `developing-incremental-models` | | "This query is slow, optimize it" | `optimizing-query-text` | +| "Add a DAG that loads the vendor file every morning" | `authoring-airflow-dags` | +| "Get these DAGs working on Airflow 3" | `migrating-to-airflow-3` | +| "Make CI fail when a DAG doesn't import" | `testing-airflow-dags` | ## Combining with Altimate MCP Tools @@ -182,6 +199,46 @@ Skills provide structured optimization with query profiling, anti-pattern detect > **Note:** This benchmark uses our internal evaluation framework. We plan to open-source it soon with additional evals. +### Airflow Skills + +Claude Code 2.1.286 with Claude Sonnet 5.5 and Claude Opus 5.5, every run graded by executing the agent's code in real Airflow 3.3.2 / 2.11.2 (DagBag import, `airflow dags test`, output data, schedule probes, planted-bug tests). Pass rate = all primary checks pass. The skills were frozen before the runs and iterated on dev cases only. + +**Hard suite**: 6 whole-project tasks (migrations, deferrable sensors, a DAG factory), 3 runs per case, arms differ only by `skills/airflow/`. The cases were selected because a Sonnet 5.5 baseline failed them, so Sonnet's baseline is biased downward; Opus 5.5 was not used for selection. Only 2 of the 6 cases are holdout. + +| Split | Model | Baseline | With skills | Paired delta [95% CI] | +|-------|-------|----------|-------------|-----------------------| +| Holdout (2 cases) | Opus 5.5 | 5/6 (83%) | **6/6 (100%)** | +0.17 [0.00, +0.33] | +| Holdout (2 cases) | Sonnet 5.5 | 4/6 (67%) | **6/6 (100%)** | +0.33 [0.00, +0.67] | +| Dev, tuned-on (4 cases) | Opus 5.5 | 9/12 (75%) | 12/12 (100%) | +0.25 [0.00, +0.50] | +| Dev, tuned-on (4 cases) | Sonnet 5.5 | 5/12 (42%) | 12/12 (100%) | +0.58 [+0.17, +1.00] | + +**Core suite**: 20 common Airflow tasks (authoring, scheduling, migration, testing, debugging), 1 run per case. Both models are near ceiling without the skills, so there is little to gain. + +| Model | Baseline | With skills | Paired delta [95% CI] | +|-------|----------|-------------|-----------------------| +| Opus 5.5 | 19/20 (95%) | 19/20 (95%) | 0.00 [-0.15, +0.15] | +| Sonnet 5.5 | 18/20 (90%) | 19/20 (95%) | +0.05 [0.00, +0.15] | + +On older, weaker models the gains were larger: with altimate-code 0.12.2 on the core suite (older skill version, 3 runs per case), Sonnet 4.6 went from 30/60 to 54/60 and Haiku 4.5 from 18/60 to 36/60 (holdout for Haiku was flat). With 6 hard cases and 2 holdout cases the intervals are wide. Per-case results, cost, trigger rates, regressions and limitations: [evals/airflow/BENCHMARK.md](evals/airflow/BENCHMARK.md). + +## Evals + +The Airflow skills ship with an open eval suite: realistic tasks in [evals/airflow/cases](evals/airflow/cases) (20 core cases plus 6 `hard-*` cases), graded by running the agent's code against pinned Airflow 3.3 and 2.11 environments. Each case is run with and without the skills (baseline vs skill A/B) using the harness in [evals/harness](evals/harness/README.md). Results and method: [evals/airflow/BENCHMARK.md](evals/airflow/BENCHMARK.md). + +```bash +evals/harness/setup_envs.sh # pinned grader + agent venvs (needs uv) +PY=~/.cache/des-evals/airflow-3.3/bin/python +$PY evals/harness/run_eval.py --cases evals/airflow/cases --self-test --split all # validate graders, no LLM +for arm in baseline skill; do + $PY evals/harness/run_eval.py --cases evals/airflow/cases --runner claude-code --arm $arm \ + --models claude-sonnet-5-5 --runs 3 --split all --out evals/airflow/results/my-$arm-run +done +# --runner altimate-code (default) uses your altimate-code provider auth instead +$PY evals/harness/report.py evals/airflow/results/my-baseline-run evals/airflow/results/my-skill-run --out evals/airflow/results/my-summary +``` + +`--runner claude-code` needs `CLAUDE_CODE_OAUTH_TOKEN` or `EVAL_CLAUDE_TOKEN_CMD`; see [CONTRIBUTING.md](CONTRIBUTING.md#airflow-evals). + ## Contributing We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. @@ -195,7 +252,6 @@ Ideas for contributions: ## Roadmap We're actively developing: -- **Airflow skills** — DAG development, debugging, testing - **Cross-platform migration** — dbt to/from SQL Server, Oracle - **Snowflake cost optimization** — Warehouse sizing, query patterns - **Data quality workflows** — Anomaly detection, freshness checks diff --git a/evals/airflow/BENCHMARK.md b/evals/airflow/BENCHMARK.md new file mode 100644 index 0000000..ee146ff --- /dev/null +++ b/evals/airflow/BENCHMARK.md @@ -0,0 +1,320 @@ +# Airflow skills benchmark (2026-10-01) + +This benchmark tests whether the three Airflow skills (`authoring-airflow-dags`, `migrating-to-airflow-3`, +`testing-airflow-dags`) help an agent produce **working** Airflow code. The final campaign used +**Claude Code 2.1.286 with Claude Sonnet 5.5 and Claude Opus 5.5**, in two arms (the same set-up with and +without the Airflow skills), on two suites: + +- **Hard suite:** 6 whole-project tasks (`hard-*`), 3 runs per case, arm and model. +- **Core suite:** 20 common Airflow tasks, 1 run per case, arm and model. + +That is 72 graded runs for the hard suite and 80 for the core suite (Sonnet 5.5 and Opus 5.5 together), +plus a trigger eval. An earlier campaign on altimate-code 0.12.2 with Sonnet 4.6 and Haiku 4.5 (older skill +version) is summarised in section 4. + +Read this first: + +- The hard suite has **6 cases, of which 2 are holdout**. Everything below about unseen tasks rests on + those 2 cases (6 runs per model and arm). The confidence intervals are wide. +- The skills were iterated against the dev cases with Sonnet 5.5, so dev numbers are **tuned-on**. +- The hard cases were kept because a Sonnet 5.5 baseline failed them in probing, so **Sonnet 5.5's baseline + is biased downward**. Opus 5.5 was not used for selection, so its numbers are the fairer read. +- On the core suite, both models are near ceiling without the skills and the skills change little. + +A run passes when all of its primary (behavioural) checks pass. The paired delta is the mean over cases of +(skill pass rate - baseline pass rate) for the same case and model. The 95% CI is a percentile bootstrap over +cases (10,000 resamples, seed 0, `evals/harness/report.py`). With 2 cases the bootstrap can only produce a +handful of distinct values, so treat those intervals as rough. + +## 1. Headline: hard suite, Claude Code + +Pass counts are runs that passed all primary checks over valid runs (every run in this campaign was valid). + +| Split | Model | Cases | Baseline | With skills | Paired Δ pass rate [95% CI] | Paired Δ primary score [95% CI] | +|---|---|---|---|---|---|---| +| **holdout** | Opus 5.5 | 2 | 5/6 (83%) | **6/6 (100%)** | +0.17 [0.00, +0.33] | +0.02 [0.00, +0.04] | +| **holdout** | Sonnet 5.5 | 2 | 4/6 (67%) | **6/6 (100%)** | +0.33 [0.00, +0.67] | +0.12 [0.00, +0.23] | +| dev (tuned-on) | Opus 5.5 | 4 | 9/12 (75%) | 12/12 (100%) | +0.25 [0.00, +0.50] | +0.02 [0.00, +0.06] | +| dev (tuned-on) | Sonnet 5.5 | 4 | 5/12 (42%) | 12/12 (100%) | +0.58 [+0.17, +1.00] | +0.16 [+0.04, +0.27] | +| all 6 cases | Opus 5.5 | 6 | 14/18 (78%) | 18/18 (100%) | +0.22 [+0.06, +0.44] | | +| all 6 cases | Sonnet 5.5 | 6 | 9/18 (50%) | 18/18 (100%) | +0.50 [+0.17, +0.83] | | + +Pooled over both models (case-clustered bootstrap, a resampled case carries both models' deltas): holdout ++0.25 [+0.17, +0.33] over 4 case-model pairs, dev +0.42 [+0.25, +0.58] over 8, all 6 cases +0.36 +[+0.22, +0.50] over 12. + +Per case, pass count (mean primary score), baseline → skills, 3 runs each: + +| Split | Case | Sonnet 5.5 | Opus 5.5 | +|---|---|---|---| +| holdout | `hard-migration-ledger-close` | 1/3 → 3/3 (0.77 → 1.00) | 3/3 → 3/3 (1.00 → 1.00) | +| holdout | `hard-deferrable-debug-partition-sensor` | 3/3 → 3/3 (1.00 → 1.00) | 2/3 → 3/3 (0.96 → 1.00) | +| dev | `hard-deferrable-sensor-retry-budget` | 2/3 → 3/3 (0.71 → 1.00) | 3/3 → 3/3 (1.00 → 1.00) | +| dev | `hard-migration-claims-partitions` | 0/3 → 3/3 (0.75 → 1.00) | 3/3 → 3/3 (1.00 → 1.00) | +| dev | `hard-migration-meter-rollups` | 3/3 → 3/3 (1.00 → 1.00) | 1/3 → 3/3 (0.93 → 1.00) | +| dev | `hard-reliability-factory-fleet` | 0/3 → 3/3 (0.92 → 1.00) | 2/3 → 3/3 (0.98 → 1.00) | + +What the data supports: + +- **With the skills, both models passed all 36 hard-suite runs.** The skill arm never failed one. +- **Opus 5.5, the fairer read:** 14/18 → 18/18 over the six cases. On the 2 holdout cases it is 5/6 → 6/6, + which is a single baseline failure (`hard-deferrable-debug-partition-sensor` run 3: the sensor no longer + deferred). That is consistent with a real effect and equally consistent with noise. The all-6-case + interval [+0.06, +0.44] excludes zero, but those cases include the tuned-on dev cases. +- **Sonnet 5.5:** the holdout gain is 4/6 → 6/6 (one case, `hard-migration-ledger-close`, 1/3 → 3/3, carries + it). Its baseline is biased downward by selection (above), so the size of its gain is overstated. +- **What the baselines got wrong** (grader details): Sonnet's `hard-migration-ledger-close` baseline lost the + third hourly extract (the case plants an `include_prior_dates` XCom cursor that returns a list on 3.x; the grader shows the third run failing), wrote the + wrong `started_by` audit value (`dag_run.external_trigger` is gone) and produced a different housekeeping + inventory. On `hard-migration-claims-partitions`, Sonnet's baseline rewrote a provider snapshot that should + not change and read the wrong directory version. On `hard-reliability-factory-fleet`, baselines left + half-built DAGs loaded or did not name broken config files in the parse output. Opus's baseline misses were + a changed `catchup` default on an asset-scheduled DAG (`hard-migration-meter-rollups`, 2 runs, one of which also hit the turn limit), a sensor that stopped deferring, and one unreported config file. +- No hard case regressed for either model. + +## 2. Core suite on the latest models + +20 cases (13 dev, 7 holdout), 1 run per case, arm and model. + +| Model | Baseline | With skills | Paired Δ pass rate [95% CI] | Paired Δ primary score [95% CI] | +|---|---|---|---|---| +| Opus 5.5 | 19/20 (95%) | 19/20 (95%) | 0.00 [-0.15, +0.15] | | +| Sonnet 5.5 | 18/20 (90%) | 19/20 (95%) | +0.05 [0.00, +0.15] | | +| Opus 5.5, holdout (7) / dev (13) | 7/7 → 7/7 / 12/13 → 12/13 | | 0.00 [0.00, 0.00] / 0.00 [-0.23, +0.23] | | +| Sonnet 5.5, holdout (7) / dev (13) | 6/7 → 7/7 / 12/13 → 12/13 | | +0.14 [0.00, +0.43] / 0.00 [0.00, 0.00] | | + +Only three cases differ between arms: + +| Split | Case | Sonnet 5.5 | Opus 5.5 | +|---|---|---|---| +| dev | `authoring-vendor-shipments-etl` | 1/1 → 1/1 | 0/1 → 1/1 | +| dev | `migration-inventory-context` | 0/1 → 0/1 | **1/1 → 0/1 (regression)** | +| holdout | `migration-shipments-assets` | 0/1 → 1/1 | 1/1 → 1/1 | + +**Interpretation.** Frontier models already handle the common Airflow tasks in this suite: version-correct +imports, idempotent loads, business-day schedules, parse-time pitfalls, pytest suites. The skills do not +change that, so the paired delta is about zero. The skills' value shows up where behaviour is +version-specific and only visible at runtime (the hard suite: 3.x manual-trigger intervals, XCom return +shapes, asset-schedule defaults, deferrable timeout budgets), and in multi-run verification such as +replaying several runs of a DAG and comparing before/after outputs. With 1 run per case the core suite +cannot resolve differences smaller than about one case. + +`migration-inventory-context` fails in 3 of 4 cells on the same primary check: the second day's `change` column +comes out blank instead of the day-over-day difference, so the Airflow 2 output is not reproduced. This is the +trap the case plants (callable parameters such as `prev_ds` are silently `None` on 3.x). Opus's baseline passed +it. Opus's skill-arm run failed it although its final message reported byte-identical output: that check called +the original task functions directly with hand-supplied values rather than replaying 2.11, which likely +explains why it did not catch the blank column (an inference from the final message, not verified in the +event stream). Treat this as one regression in a single run, not as a measured rate. + +## 3. Trigger eval (Claude Code) + +30 short queries (18 should-fire, 6 per skill, and 12 near-misses such as dbt, Prefect, Dagster, cron, +plain pytest and GitHub Actions), 2 runs per query and model, 4 turns each. A run is a hit when the expected +skill fired. Source: [`results/final-triggers-2026-10-01/REPORT.md`](results/final-triggers-2026-10-01/REPORT.md). +That run also included Fable 5.1; its rows are left out here (see section 7). + +| Skill | Sonnet 5.5 recall | Opus 5.5 recall | Both models | Precision | Wrong Airflow skill fired | +|---|---|---|---|---|---| +| authoring-airflow-dags | 7/12 (58%) | 12/12 (100%) | 19/24 (79%) | 100% | 0 | +| migrating-to-airflow-3 | 12/12 (100%) | 12/12 (100%) | 24/24 (100%) | 100% | 0 | +| testing-airflow-dags | 11/12 (92%) | 12/12 (100%) | 23/24 (96%) | 100% | 0 | + +- No Airflow skill fired on any of the 48 near-miss runs (0/24 per model). +- All 6 misses were Sonnet 5.5 firing no skill: `author-02` (import error in the UI, run 1), `author-05` (make + the events job rerun-safe, runs 1 and 2), `author-06` (pass a row count between steps, runs 1 and 2) and + `test-06` (end-to-end test on sample data, run 1). These queries describe the symptom and do not use + Airflow vocabulary, which is where a model is least likely to reach for a skill. +- Within the task runs of section 1 and 2, the skill arm used an Airflow skill in every run of both models + (the per-case reports list the skill used). + +## 4. Legacy: altimate-code 0.12.2, Sonnet 4.6 and Haiku 4.5, core suite + +An earlier campaign (2026-09-30) ran the 20 core cases, 3 runs per case, in altimate-code 0.12.2 through +Vertex, with an **older version of the skills** (`skills/airflow` sha256 `e03e9d6d…`, not the frozen +version above). Those runs shared `/tmp` between parallel runs, so the contamination scan of the current +harness was applied retroactively +([`results/altimate-code-2026-09-30-summary/`](results/altimate-code-2026-09-30-summary/REPORT.md); manual review +in `REVIEW.md` there). The scan flagged 97 of 240 attempts; manual review found no passing run that used another +attempt's code or output. The one confirmed cross-run interference was an old skill instruction that wrote previews +to fixed shared paths, so parallel skill-arm runs overwrote each other's previews and some passing runs read another +case's preview. Its effect on pass rates is unknown in sign and size (the review judged it more likely to add noise +than to inflate passes). The instruction now uses `$TMPDIR`; the exclusion analysis below is a sensitivity check. + +Pass counts, baseline → skill (all runs, then excluding flagged runs), and paired Δ with 95% CI: + +| Split | Model | All runs | Excluding flagged runs | +|---|---|---|---| +| holdout (7 cases) | Sonnet 4.6 | 8/21 → 17/21: Δ +0.43 [+0.05, +0.76] | 7/19 → 9/10: Δ +0.33 [-0.17, +0.83] | +| holdout (7 cases) | Haiku 4.5 | 4/21 → 5/21: Δ +0.05 [0.00, +0.14] | 0/10 → 4/13: Δ +0.17 [0.00, +0.50] | +| dev, tuned-on (13) | Sonnet 4.6 | 22/39 → 37/39: Δ +0.38 [+0.15, +0.62] | 14/30 → 16/17: Δ +0.22 [+0.06, +0.39] | +| dev, tuned-on (13) | Haiku 4.5 | 14/39 → 31/39: Δ +0.44 [+0.18, +0.67] | 8/20 → 18/24: Δ +0.43 [+0.09, +0.78] | + +Overall, all runs: Sonnet 4.6 30/60 (50%) → 54/60 (90%); Haiku 4.5 18/60 (30%) → 36/60 (60%). The gains on +older and smaller models are large on dev and, for Sonnet 4.6, on holdout. For Haiku 4.5 the holdout gain is +not distinguishable from zero. Excluding flagged runs removes most skill-arm passes (the flags there come from +the fixed-path previews), so that column is a stress test with a known bias, and its holdout intervals include +zero for Sonnet 4.6. Read this next to section 2 with care: the two campaigns differ in runner, skill +version, models and harness isolation, so the comparison cannot isolate model strength. It is consistent with +older and smaller models gaining more on common tasks than the latest models do, not proof of it. + +## 5. Cost and time per run (API-equivalent USD) + +Runs used subscription plans, so nothing was billed per token. `total_cost_usd` from Claude Code is the +API-equivalent list-price cost. Wall time is the agent's wall clock and excludes grading. Costs include +retried attempts. Cost per passing run is total spend over passing runs. + +| Suite | Model | Arm | Mean cost / run | Cost / passing run | Mean wall / run | Total | +|---|---|---|---|---|---|---| +| hard | Sonnet 5.5 | baseline | $0.62 | $1.24 | 604 s | $11.17 | +| hard | Sonnet 5.5 | skill | $0.43 | $0.43 | 316 s | $7.82 | +| hard | Opus 5.5 | baseline | $2.00 | $2.57 | 941 s | $35.99 | +| hard | Opus 5.5 | skill | $1.36 | $1.36 | 695 s | $24.43 | +| core | Sonnet 5.5 | baseline | $0.135 | $0.150 | 59 s | $2.70 | +| core | Sonnet 5.5 | skill | $0.214 | $0.225 | 162 s | $4.28 | +| core | Opus 5.5 | baseline | $0.473 | $0.498 | 267 s | $9.46 | +| core | Opus 5.5 | skill | $0.583 | $0.614 | 309 s | $11.66 | + +On the hard suite the skill arm was cheaper and faster per run, and on the core suite it added cost (+58% per +run for Sonnet 5.5, +23% for Opus 5.5) and wall time. A plausible explanation (a hypothesis, not a measured +breakdown) is that baselines on hard cases spend long sessions on runtime failures, while on core cases the +agent reads the skill and runs its verification scripts. The campaign's total for the two models was $79.41 (hard) +plus $28.09 (core), API-equivalent. + +## 6. Method + +**Arms.** Both arms are identical except for `skills/airflow/`. Both stage a copy of the repo skills (dbt, +Snowflake, Databricks, altimate-code); the skill arm's copy adds the three Airflow skills. The Claude Code +inventories show 29 skills in the baseline arm and 32 in the skill arm. + +**Isolation (Claude Code runner).** +- Each attempt gets a fresh `CLAUDE_CONFIG_DIR`, so the host's `~/.claude` settings, hooks, plugins, MCP + servers, skills and memory never load. `CLAUDE.md` and `AGENTS.md` loading is disabled; MCP is empty. +- Each attempt runs in a fresh git-initialised copy of the case fixture, with its own `TMPDIR`, under a macOS + `sandbox-exec` profile: writes only to the attempt's scratch dir and the agent venv, reads denied for the + repo checkout, results, other runs' scratch dirs and the host's Claude Code files, and **signals denied + except within the same sandbox instance**, so an agent's `pkill` cannot reach the harness, graders or sibling + runs. +- The agent works in a separate **agent venv** (Airflow 3.3.2 or 2.11.2, Python 3.12) that the harness checks + against a manifest before and after every attempt and restores if changed. **Graders run in different + grader venvs** that the agent is never pointed at. +- **Inventory verified from the `system/init` event** of every attempt: expected repo skills present, Airflow + skills only in the skill arm, no MCP server, no unexpected skill or memory file. A mismatch kills the run and + stops the campaign. This caught a real problem when the Fable 5.1 baseline resumed under a second + subscription account: that account provisioned two extra skills (`deep-research`, `workflow-authoring`), the + check aborted the run, and the campaign stopped there rather than mix inventories. +- A contamination scan and a broad-kill-command detector run on every attempt (see Limitations for what they + flagged). + +**Grading.** Each case's `grade.py` executes the agent's final workspace in real Apache Airflow **3.3.2** or +**2.11.2** (Python 3.12.8): DagBag import, `airflow dags test` with DagRun state read from the DB, output data +compared with values computed from the pristine fixture, timetable and data-interval probes, reruns for +idempotency, replays of scheduled, asset-triggered and CLI-triggered runs against the Airflow 2.11 output, and, +for testing cases, the agent's pytest suite run against hidden planted bugs. Secondary checks (verification +commands run, ruff AIR, deprecations) do not affect pass or fail. The agent's own claims are never trusted. + +**Grader validation (self-test, re-run for this report, all 26 cases).** `run_eval.py --self-test --split all +--parallel 4` ran **239 variants with 0 violations**: 26 references and 26 alternate references (all must +pass), 26 untouched fixtures and 161 mutants (all must fail at least one primary check). That is 105 mutants +over the 20 core cases and 56 over the 6 hard cases. Output: +[`results/selftest-final/`](results/selftest-final/selftest.txt). + +**Hard-case design.** The hard cases are whole-project tasks with runtime-only traps that pass import and lint +checks and only show up when runs are replayed. Each was probed against a **Sonnet 5.5 baseline**, and only +cases the baseline failed for a genuine reason were kept, so Sonnet 5.5's baseline is biased downward by +construction. Opus 5.5 was not used for selection. Each case ships a reference and an independently written +alternate reference that pass, plus mutants that fail, and the primary checks follow from the prompt and +fixture. + +**Dev/holdout blindness.** The skills were written and iterated on dev cases with Sonnet 5.5 only. Skill +writers did not read holdout prompts, fixtures or graders. The core suite has 13 dev and 7 holdout cases; the +hard suite 4 dev and 2 holdout. Holdout is the only generalisation evidence, and dev is reported as tuned-on. + +**Statistics.** Pass rates count valid runs only (no run was excluded in this campaign). Timeouts, turn +limits and cost caps count as failures. Paired deltas are per case and model, and CIs bootstrap over cases. + +## 7. Limitations + +- **Small n.** 6 hard cases, 2 of them holdout, 3 runs each. One run moves a case's pass rate by 33 points, + and the core suite has 1 run per case. The holdout intervals include zero for both models. +- **Selection bias.** Hard cases were chosen by a Sonnet 5.5 baseline failing them. This inflates Sonnet's + baseline gap. Case selection was not repeated with Opus 5.5 or with the skills in place. +- **One Airflow target.** All 6 hard cases target Airflow 3.3. +- **Correlated traps.** Several migration cases share the same trap (the XCom `include_prior_dates` cursor + returning a list on 3.x, manual-trigger intervals), and the skills carry a reference for it + (`runtime-traps.md`). Their passes are not independent evidence of generalisation across traps. +- **Skills tuned on Sonnet 5.5 dev cases.** Dev numbers are upper bounds. +- **Shared agent venv under `--parallel 4`.** Concurrent attempts share one writable agent venv per Airflow + version; the harness checks and restores it between attempts, but an install by one run could be visible to + another until then. The final campaign recorded no agent-venv changes or restores. +- **Resume provenance.** The hard campaigns were resumed several times. The harness did not then check that + case or skill hashes were unchanged across a resume (it does now). The evidence of no drift is that the + skills hash in the campaign state file is identical before, during and after, and that every final result + directory's recorded case hashes equal the current case directories (104 of 104). +- **Single machine, macOS.** Everything ran on one Mac with `sandbox-exec`; nothing was run on Linux CI + runners. The sandbox profile is macOS-specific. +- **Claude Code version changed mid-campaign.** All Sonnet 5.5 and Opus 5.5 runs reported here + (`claude_code_version` in every attempt record) ran on **2.1.286**. The CLI auto-updated to 2.1.287 on + 2026-10-01 around 11:01 PT, after those runs; only the Fable 5.1 runs below used 2.1.287. +- **Two subscription accounts.** The campaign moved to a second account when the first reached its weekly + limit during the Fable runs. The reported Sonnet 5.5 and Opus 5.5 results were all run before that switch. + Costs are API-equivalent estimates, not billing. +- **Fable 5.1 skipped (partial, not reported).** Partial hard-suite runs exist in + `results/final-hard-{baseline,skill}-claude-fable-5-1-2026-10-01` and are not used for any conclusion. The + skill arm finished runs 1-2 and the baseline arm finished run 1; the baseline's run 2 was aborted by the + inventory check and run 3 never ran, then Fable was dropped from the scope. The trigger eval's Fable rows are + excluded from section 3. +- **Contamination scan and kill commands flagged many runs.** In the final Sonnet 5.5 and Opus 5.5 runs, 15 + hard-suite and 14 core-suite runs were flagged, and 29 hard-suite and 5 core-suite runs ran a broad + `pkill`. Inspected flags were the literal `/tmp` fallback in `${TMPDIR:-/tmp}` (written by the migration + skill), agents addressing their own scratch dir by absolute path, and one Opus run + (`hard-migration-meter-rollups`, skill arm, run 3, passed) that used the Airflow 2.11 grader venv's + interpreter. One Sonnet 5.5 skill-arm run (`migration-hourly-pageviews-templates`) mentioned the staged skill + by a `~/.claude/skills/...` path. Every `pkill` ran under the signal-isolated sandbox, so it could not reach other runs. Not + every flag was reviewed by hand. Excluding all flagged runs leaves the picture unchanged (hard, baseline → + skills: Sonnet 5/11 → 15/15, Opus 12/16 → 15/15; core: Sonnet 16/17 → 10/10, Opus 19/20 → 18/19), but it + leaves few pairs. +- **Regressions with causes.** On the hard suite the skill arm never lost a run. On the core suite, Opus 5.5 + lost `migration-inventory-context` (see section 2): the migrated DAG's `change` column came out blank, and its + self-check did not replay 2.11. The same case also failed for Sonnet 5.5 in both arms, so the migration skill does not + reliably close this trap. Skill-arm + secondary scores matched or exceeded baseline on both suites. +- **Triggering depends on phrasing.** Sonnet 5.5 missed the skill on queries with no Airflow vocabulary + (section 3); recall is a 4-turn measure. +- **`final.md` content was not evaluated.** Grading never reads the agent's final message. + +## 8. Reproducibility + +| Item | Value | +|---|---| +| Agent | Claude Code 2.1.286 (`--runner claude-code`), models `claude-sonnet-5-5`, `claude-opus-5-5` | +| Airflow 3.3 env | apache-airflow 3.3.2, Python 3.12.8, pytest 9.1.1, ruff 0.16.9, duckdb 1.5.6, pandas 3.0.5 | +| Airflow 2.11 env | apache-airflow 2.11.2, Python 3.12.8 | +| Host | macOS 26 arm64, single machine | +| `skills/airflow` sha256 | `d3af9fac9ff0337042ffb634539cd96828c2f63020d1651fee7fe70fd09557a0` (identical in the campaign state file before and after the campaign, in every skill-arm `meta.json`, and in the working tree when this report was written; computed by `run_eval.hash_dir`, bytecode ignored) | +| Staged skills sha256 | baseline `ca2b482d…`, skill `45fa68a5…` | +| Runs | 2026-10-01 09:29-16:02 UTC (Sonnet 5.5 and Opus 5.5 final runs) | +| Results | `results/final-{hard,core}-{baseline,skill}-claude-{sonnet,opus}-5-5-2026-10-01`, merged in `results/final-summary-2026-10-01/{hard,core}/` | +| Triggers | `results/final-triggers-2026-10-01/` | + +```bash +evals/harness/setup_envs.sh +PY=~/.cache/des-evals/airflow-3.3/bin/python +$PY evals/harness/run_eval.py --cases evals/airflow/cases --self-test --split all --parallel 4 \ + --out evals/airflow/results/selftest-final +# one arm, one model, hard suite (set CLAUDE_CODE_OAUTH_TOKEN or EVAL_CLAUDE_TOKEN_CMD first) +$PY evals/harness/run_eval.py --runner claude-code --cases evals/airflow/cases --arm skill \ + --models claude-sonnet-5-5 --runs 3 --parallel 4 --split all \ + --case hard-migration-ledger-close --case hard-deferrable-debug-partition-sensor \ + --case hard-deferrable-sensor-retry-budget --case hard-migration-claims-partitions \ + --case hard-migration-meter-rollups --case hard-reliability-factory-fleet \ + --out evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01 +# repeat with --arm baseline; core suite = the 20 non-hard cases, --runs 1 +$PY evals/harness/report.py evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01 \ + evals/airflow/results/final-hard-skill-claude-sonnet-5-5-2026-10-01 \ + evals/airflow/results/final-hard-baseline-claude-opus-5-5-2026-10-01 \ + evals/airflow/results/final-hard-skill-claude-opus-5-5-2026-10-01 \ + --out evals/airflow/results/final-summary-2026-10-01/hard +``` + +Full per-arm rows are in `results/final-summary-2026-10-01/{hard,core}/REPORT.md` and `results.json`. diff --git a/evals/airflow/cases/_example_smoke/case.yaml b/evals/airflow/cases/_example_smoke/case.yaml new file mode 100644 index 0000000..5248466 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/case.yaml @@ -0,0 +1,19 @@ +id: _example_smoke +area: authoring +split: dev +airflow_version: "3.3" +max_turns: 25 +timeout_s: 600 +prompt: | + We need a new daily Airflow DAG called `event_counts`. It should read + data/events.csv, count the events per event_type for the rows whose + event_date equals the run's date, and write the result to + output/event_counts/.csv with the columns event_type,count, + sorted by event_type. Put it in dags/ next to the existing DAG and follow the + same conventions. +description: | + Harness smoke case (not part of the benchmark; ids starting with "_" only run + when named with --case). Tests that a new Airflow 3.3 DAG imports, runs under + `airflow dags test`, writes correct per-day output and is idempotent on a + re-run. Mutant `airflow2_imports` uses Airflow 2 idioms (days_ago, + schedule_interval) that break on 3.3. diff --git a/evals/airflow/cases/_example_smoke/fixture/dags/orders_daily.py b/evals/airflow/cases/_example_smoke/fixture/dags/orders_daily.py new file mode 100644 index 0000000..1c526e0 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/fixture/dags/orders_daily.py @@ -0,0 +1,32 @@ +"""Daily order totals, written as one CSV per logical date.""" + +from __future__ import annotations + +import csv +from datetime import datetime +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="@daily", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["orders"], +) +def orders_daily(): + @task + def summarize(ds=None): + out = PROJECT_ROOT / "output" / "orders_daily" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + csv.writer(fh).writerow(["order_date", "orders"]) + return str(out) + + summarize() + + +orders_daily() diff --git a/evals/airflow/cases/_example_smoke/fixture/data/events.csv b/evals/airflow/cases/_example_smoke/fixture/data/events.csv new file mode 100644 index 0000000..c091264 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/fixture/data/events.csv @@ -0,0 +1,11 @@ +event_id,event_date,event_type,user_id +1,2026-02-28,click,u1 +2,2026-02-28,view,u2 +3,2026-03-01,click,u1 +4,2026-03-01,view,u3 +5,2026-03-01,click,u2 +6,2026-03-01,purchase,u1 +7,2026-03-02,view,u4 +8,2026-03-02,click,u2 +9,2026-03-01,view,u5 +10,2026-03-01,click,u4 diff --git a/evals/airflow/cases/_example_smoke/fixture/requirements.txt b/evals/airflow/cases/_example_smoke/fixture/requirements.txt new file mode 100644 index 0000000..b1dd561 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==3.3.2 diff --git a/evals/airflow/cases/_example_smoke/grade.py b/evals/airflow/cases/_example_smoke/grade.py new file mode 100644 index 0000000..3777bf8 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/grade.py @@ -0,0 +1,78 @@ +"""Grader for _example_smoke (template for case authors). + +Run: grade.py --workspace DIR --events EVENTS.jsonl --out RESULT.json +""" + +from __future__ import annotations + +import csv +import os +import shutil +import sys +from collections import Counter +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "event_counts" +DAY, NEIGHBOUR = "2026-03-01", "2026-03-02" + + +def expected_rows(day: str) -> list[list[str]]: + """Ground truth from the pristine fixture data (never from the agent's workspace).""" + counts: Counter[str] = Counter() + with (CASE_DIR / "fixture" / "data" / "events.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["event_date"] == day: + counts[row["event_type"]] += 1 + return [["event_type", "count"]] + [[k, str(counts[k])] for k in sorted(counts)] + + +def read_rows(path: Path) -> list[list[str]] | None: + if not path.exists(): + return None + with path.open(newline="") as fh: + return [[c.strip() for c in r] for r in csv.reader(fh) if r] + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + + out_dir = ws / "output" / DAG_ID + # Never grade files the agent produced by hand: only what the DAG writes counts. + shutil.rmtree(ws / "output", ignore_errors=True) + if dag is not None: + env = g.airflow_env(ws) + neighbour = g.run_dags_test(py, ws, DAG_ID, NEIGHBOUR, env=env) + first = g.run_dags_test(py, ws, DAG_ID, DAY, env=env) + grader.primary("airflow dags test succeeds", neighbour.ok and first.ok, + "" if first.ok and neighbour.ok else (first.log if not first.ok else neighbour.log)[-3000:]) + got = read_rows(out_dir / f"{DAY}.csv") + grader.primary("output for the run date is correct", got == expected_rows(DAY), + f"expected {expected_rows(DAY)}, got {got}") + second = g.run_dags_test(py, ws, DAG_ID, DAY, env=env) + again = read_rows(out_dir / f"{DAY}.csv") + grader.primary("re-run is idempotent and preserves other dates", + second.ok and again == expected_rows(DAY) + and read_rows(out_dir / f"{NEIGHBOUR}.csv") == expected_rows(NEIGHBOUR), + f"second run ok={second.ok}; after re-run {DAY}={again}; " + f"{NEIGHBOUR}={read_rows(out_dir / f'{NEIGHBOUR}.csv')}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/_example_smoke/mutants/airflow2_imports/dags/event_counts.py b/evals/airflow/cases/_example_smoke/mutants/airflow2_imports/dags/event_counts.py new file mode 100644 index 0000000..4040f80 --- /dev/null +++ b/evals/airflow/cases/_example_smoke/mutants/airflow2_imports/dags/event_counts.py @@ -0,0 +1,36 @@ +"""Daily count of events per event type (Airflow 2 idioms: fails to import on 3.3).""" + +from __future__ import annotations + +import csv +from collections import Counter +from pathlib import Path + +from airflow.decorators import dag, task +from airflow.utils.dates import days_ago + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag(schedule_interval="@daily", start_date=days_ago(1), catchup=False, tags=["events"]) +def event_counts(): + @task + def count_events(ds=None): + counts: Counter[str] = Counter() + with (PROJECT_ROOT / "data" / "events.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["event_date"] == ds: + counts[row["event_type"]] += 1 + out = PROJECT_ROOT / "output" / "event_counts" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["event_type", "count"]) + for event_type in sorted(counts): + writer.writerow([event_type, counts[event_type]]) + return str(out) + + count_events() + + +event_counts() diff --git a/evals/airflow/cases/_example_smoke/reference/dags/event_counts.py b/evals/airflow/cases/_example_smoke/reference/dags/event_counts.py new file mode 100644 index 0000000..ffdba2b --- /dev/null +++ b/evals/airflow/cases/_example_smoke/reference/dags/event_counts.py @@ -0,0 +1,41 @@ +"""Daily count of events per event type.""" + +from __future__ import annotations + +import csv +from collections import Counter +from datetime import datetime +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="@daily", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["events"], +) +def event_counts(): + @task + def count_events(ds=None): + counts: Counter[str] = Counter() + with (PROJECT_ROOT / "data" / "events.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["event_date"] == ds: + counts[row["event_type"]] += 1 + out = PROJECT_ROOT / "output" / "event_counts" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["event_type", "count"]) + for event_type in sorted(counts): + writer.writerow([event_type, counts[event_type]]) + return str(out) + + count_events() + + +event_counts() diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/alt_reference/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/alt_reference/dags/inventory_snapshot.py new file mode 100644 index 0000000..93c2946 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/alt_reference/dags/inventory_snapshot.py @@ -0,0 +1,55 @@ +"""Inventory snapshot DAG written with TaskFlow and pandas.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from pathlib import Path + +from airflow.decorators import dag, task +from airflow.operators.empty import EmptyOperator + +ROOT = Path(__file__).resolve().parent.parent + + +@dag( + schedule="30 2 * * *", + start_date=datetime(2026, 8, 15, tzinfo=timezone.utc), + catchup=False, + max_active_runs=1, + tags=["inventory"], +) +def inventory_snapshot(): + @task + def snapshot(ds=None) -> str: + import pandas as pd + + moves = pd.read_csv(ROOT / "data" / "inventory_movements.csv") + moves = moves[moves["movement_date"] <= ds] + stock = ( + moves.groupby(["warehouse_id", "sku"], as_index=False)["qty_change"].sum() + .rename(columns={"qty_change": "on_hand"}) + .sort_values(["warehouse_id", "sku"]) + ) + out = ROOT / "output" / "inventory_snapshot" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + stock.to_csv(out, index=False) + return str(out) + + @task + def low_stock(snapshot_path: str, ds=None) -> str: + import pandas as pd + + stock = pd.read_csv(snapshot_path) + points = pd.read_csv(ROOT / "data" / "reorder_points.csv") + low = stock.merge(points, on="sku") + low = low[low["on_hand"] < low["reorder_point"]].sort_values(["warehouse_id", "sku"]) + out = ROOT / "output" / "low_stock" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + low[["warehouse_id", "sku", "on_hand", "reorder_point"]].to_csv(out, index=False) + return str(out) + + done = EmptyOperator(task_id="done") + low_stock(snapshot()) >> done + + +inventory_snapshot() diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/case.yaml b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/case.yaml new file mode 100644 index 0000000..4054094 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/case.yaml @@ -0,0 +1,38 @@ +id: authoring-airflow2-inventory-snapshot +area: authoring +split: dev +airflow_version: "2.11" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1500 +prompt: | + Can you add a DAG called `inventory_snapshot`? It should run every day at + 02:30 UTC and do two things for the run's date: + + 1. Work out the end-of-day stock on hand per warehouse and SKU as of that + date, i.e. the sum of qty_change over every movement in + data/inventory_movements.csv up to and including that date. Write it to + output/inventory_snapshot/.csv with the columns + warehouse_id,sku,on_hand. + 2. After that, write output/low_stock/.csv with the rows + (warehouse_id,sku,on_hand,reorder_point) where on_hand is below the SKU's + reorder point from data/reorder_points.csv. + + Sort both files by warehouse_id then sku. We need to be able to rerun any + date since 2026-09-01, but the DAG must not backfill old dates on its own + when it gets deployed. Keep it consistent with the existing DAG. +description: | + Version-aware authoring for a project pinned to Airflow 2.11 (only + requirements.txt and the existing DAG's imports reveal the version). Pain + point P1 in reverse: agents write Airflow 3 idioms (`airflow.sdk`, + `airflow.providers.standard.*`) that do not exist in 2.11, and forget that + 2.11 defaults `catchup` to True. Primary (all in the 2.11 env): DAG imports; + timetable probe shows runs at 02:30 UTC daily; catchup is False; `airflow dags + test` for two logical dates writes the correct snapshot and low-stock files + (zero-stock snapshot rows may be omitted); a rerun is idempotent. The grader + swaps in grader_data/inventory_movements.csv: the sample movements plus a + warehouse whose stock sits exactly at the reorder point and moves on the run + date. + Mutants: `from airflow.sdk import dag, task`, standard-provider operator + import, catchup omitted, strict `<` date filter that drops the run date's + movements, low stock flagged with <= instead of <. diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/.gitignore b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/README.md b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/README.md new file mode 100644 index 0000000..13827ac --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/README.md @@ -0,0 +1,10 @@ +# supply-chain-dags + +DAGs for the supply-chain analytics team, deployed to our managed Airflow +environment (versions are pinned in `requirements.txt`). + +- `dags/`: DAG files +- `data/`: nightly CSV extracts (`inventory_movements.csv`, `reorder_points.csv`, `sales.csv`) +- `output//.csv`: DAG outputs, one file per logical date + +Try a DAG locally with `airflow dags test `. diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/dags/sales_daily.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/dags/sales_daily.py new file mode 100644 index 0000000..1f115ca --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/dags/sales_daily.py @@ -0,0 +1,53 @@ +"""Daily sales rollup per store. + +Convention for this repo: every daily DAG writes one file per logical date +(`{{ ds }}`) under output//, so a rerun simply overwrites that day. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task +from airflow.operators.empty import EmptyOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +@dag( + dag_id="sales_daily", + schedule="15 1 * * *", + start_date=pendulum.datetime(2026, 8, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["sales"], +) +def sales_daily(): + start = EmptyOperator(task_id="start") + + @task + def rollup(ds=None) -> str: + totals: dict[str, float] = defaultdict(float) + with (DATA_DIR / "sales.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["sale_date"] == ds: + totals[row["store_id"]] += float(row["amount"]) + out = OUTPUT_DIR / "sales_daily" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["store_id", "revenue"]) + for store in sorted(totals): + writer.writerow([store, round(totals[store], 2)]) + return str(out) + + start >> rollup() + + +sales_daily() diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/inventory_movements.csv b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/inventory_movements.csv new file mode 100644 index 0000000..3234320 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/inventory_movements.csv @@ -0,0 +1,427 @@ +movement_id,warehouse_id,sku,movement_date,qty_change +MV-00001,AMS1,SKU-001,2026-08-25,66 +MV-00002,AMS1,SKU-002,2026-08-25,44 +MV-00003,AMS1,SKU-003,2026-08-25,75 +MV-00004,AMS1,SKU-004,2026-08-25,31 +MV-00005,AMS1,SKU-005,2026-08-25,34 +MV-00006,AMS1,SKU-006,2026-08-25,37 +MV-00007,AMS1,SKU-007,2026-08-25,71 +MV-00008,AMS1,SKU-008,2026-08-25,32 +MV-00009,BER2,SKU-001,2026-08-25,89 +MV-00010,BER2,SKU-002,2026-08-25,52 +MV-00011,BER2,SKU-003,2026-08-25,29 +MV-00012,BER2,SKU-004,2026-08-25,36 +MV-00013,BER2,SKU-005,2026-08-25,80 +MV-00014,BER2,SKU-006,2026-08-25,78 +MV-00015,BER2,SKU-007,2026-08-25,33 +MV-00016,BER2,SKU-008,2026-08-25,55 +MV-00017,LON1,SKU-001,2026-08-25,36 +MV-00018,LON1,SKU-002,2026-08-25,79 +MV-00019,LON1,SKU-003,2026-08-25,32 +MV-00020,LON1,SKU-004,2026-08-25,40 +MV-00021,LON1,SKU-005,2026-08-25,53 +MV-00022,LON1,SKU-006,2026-08-25,32 +MV-00023,LON1,SKU-007,2026-08-25,75 +MV-00024,LON1,SKU-008,2026-08-25,31 +MV-00025,AMS1,SKU-003,2026-08-26,14 +MV-00026,LON1,SKU-002,2026-08-26,-9 +MV-00027,LON1,SKU-003,2026-08-26,28 +MV-00028,LON1,SKU-004,2026-08-26,-9 +MV-00029,LON1,SKU-002,2026-08-26,-10 +MV-00030,AMS1,SKU-008,2026-08-26,-7 +MV-00031,BER2,SKU-008,2026-08-26,-8 +MV-00032,BER2,SKU-005,2026-08-26,15 +MV-00033,LON1,SKU-004,2026-08-26,19 +MV-00034,BER2,SKU-006,2026-08-27,-5 +MV-00035,LON1,SKU-002,2026-08-27,23 +MV-00036,AMS1,SKU-006,2026-08-27,25 +MV-00037,BER2,SKU-001,2026-08-27,-2 +MV-00038,LON1,SKU-006,2026-08-27,21 +MV-00039,LON1,SKU-008,2026-08-27,-8 +MV-00040,AMS1,SKU-002,2026-08-27,-8 +MV-00041,LON1,SKU-002,2026-08-27,32 +MV-00042,BER2,SKU-008,2026-08-27,22 +MV-00043,LON1,SKU-006,2026-08-27,24 +MV-00044,BER2,SKU-003,2026-08-27,-8 +MV-00045,AMS1,SKU-004,2026-08-27,-3 +MV-00046,LON1,SKU-004,2026-08-27,-15 +MV-00047,BER2,SKU-002,2026-08-27,22 +MV-00048,BER2,SKU-003,2026-08-28,-14 +MV-00049,LON1,SKU-005,2026-08-28,-6 +MV-00050,LON1,SKU-007,2026-08-28,-3 +MV-00051,AMS1,SKU-003,2026-08-28,31 +MV-00052,AMS1,SKU-001,2026-08-28,-10 +MV-00053,AMS1,SKU-005,2026-08-28,14 +MV-00054,BER2,SKU-006,2026-08-28,-6 +MV-00055,AMS1,SKU-001,2026-08-28,-14 +MV-00056,LON1,SKU-007,2026-08-28,-7 +MV-00057,AMS1,SKU-008,2026-08-28,-1 +MV-00058,AMS1,SKU-002,2026-08-28,-8 +MV-00059,AMS1,SKU-002,2026-08-28,11 +MV-00060,AMS1,SKU-001,2026-08-28,-9 +MV-00061,AMS1,SKU-006,2026-08-28,-2 +MV-00062,LON1,SKU-007,2026-08-29,18 +MV-00063,BER2,SKU-006,2026-08-29,-2 +MV-00064,BER2,SKU-008,2026-08-29,-5 +MV-00065,AMS1,SKU-003,2026-08-29,20 +MV-00066,LON1,SKU-005,2026-08-29,-12 +MV-00067,AMS1,SKU-001,2026-08-29,40 +MV-00068,LON1,SKU-006,2026-08-29,27 +MV-00069,AMS1,SKU-005,2026-08-29,-14 +MV-00070,AMS1,SKU-005,2026-08-29,-15 +MV-00071,BER2,SKU-004,2026-08-30,-13 +MV-00072,LON1,SKU-006,2026-08-30,-10 +MV-00073,AMS1,SKU-004,2026-08-30,-12 +MV-00074,AMS1,SKU-004,2026-08-30,-6 +MV-00075,LON1,SKU-001,2026-08-30,-13 +MV-00076,BER2,SKU-008,2026-08-30,32 +MV-00077,LON1,SKU-006,2026-08-30,-15 +MV-00078,LON1,SKU-006,2026-08-30,-6 +MV-00079,AMS1,SKU-002,2026-08-31,16 +MV-00080,BER2,SKU-004,2026-08-31,-15 +MV-00081,LON1,SKU-001,2026-08-31,-11 +MV-00082,BER2,SKU-002,2026-08-31,-2 +MV-00083,BER2,SKU-004,2026-08-31,-3 +MV-00084,BER2,SKU-006,2026-08-31,40 +MV-00085,LON1,SKU-007,2026-08-31,-12 +MV-00086,LON1,SKU-003,2026-09-01,14 +MV-00087,AMS1,SKU-003,2026-09-01,-8 +MV-00088,LON1,SKU-003,2026-09-01,-10 +MV-00089,BER2,SKU-006,2026-09-01,27 +MV-00090,AMS1,SKU-001,2026-09-01,33 +MV-00091,LON1,SKU-002,2026-09-01,-15 +MV-00092,AMS1,SKU-007,2026-09-01,-4 +MV-00093,AMS1,SKU-005,2026-09-02,26 +MV-00094,AMS1,SKU-006,2026-09-02,23 +MV-00095,AMS1,SKU-001,2026-09-02,-6 +MV-00096,BER2,SKU-007,2026-09-02,-15 +MV-00097,LON1,SKU-003,2026-09-02,-9 +MV-00098,LON1,SKU-001,2026-09-02,-11 +MV-00099,AMS1,SKU-001,2026-09-02,-3 +MV-00100,AMS1,SKU-003,2026-09-02,-12 +MV-00101,AMS1,SKU-001,2026-09-02,26 +MV-00102,LON1,SKU-008,2026-09-03,-2 +MV-00103,LON1,SKU-001,2026-09-03,18 +MV-00104,AMS1,SKU-002,2026-09-03,-9 +MV-00105,AMS1,SKU-002,2026-09-03,-10 +MV-00106,LON1,SKU-004,2026-09-03,-8 +MV-00107,LON1,SKU-008,2026-09-03,-4 +MV-00108,LON1,SKU-005,2026-09-03,-15 +MV-00109,AMS1,SKU-008,2026-09-03,13 +MV-00110,BER2,SKU-008,2026-09-03,31 +MV-00111,AMS1,SKU-007,2026-09-03,31 +MV-00112,BER2,SKU-002,2026-09-03,-3 +MV-00113,LON1,SKU-006,2026-09-03,38 +MV-00114,AMS1,SKU-008,2026-09-03,40 +MV-00115,AMS1,SKU-007,2026-09-03,-3 +MV-00116,AMS1,SKU-007,2026-09-04,-7 +MV-00117,BER2,SKU-007,2026-09-04,20 +MV-00118,AMS1,SKU-006,2026-09-04,27 +MV-00119,BER2,SKU-008,2026-09-04,-7 +MV-00120,BER2,SKU-005,2026-09-04,-2 +MV-00121,AMS1,SKU-004,2026-09-04,-2 +MV-00122,AMS1,SKU-005,2026-09-04,38 +MV-00123,AMS1,SKU-005,2026-09-04,-14 +MV-00124,BER2,SKU-005,2026-09-04,-9 +MV-00125,LON1,SKU-008,2026-09-05,-2 +MV-00126,BER2,SKU-001,2026-09-05,-3 +MV-00127,BER2,SKU-002,2026-09-05,10 +MV-00128,LON1,SKU-002,2026-09-05,-2 +MV-00129,LON1,SKU-004,2026-09-05,37 +MV-00130,AMS1,SKU-008,2026-09-05,27 +MV-00131,BER2,SKU-005,2026-09-05,-1 +MV-00132,LON1,SKU-004,2026-09-05,-3 +MV-00133,BER2,SKU-001,2026-09-05,39 +MV-00134,BER2,SKU-005,2026-09-05,-4 +MV-00135,BER2,SKU-008,2026-09-05,-3 +MV-00136,BER2,SKU-006,2026-09-05,-5 +MV-00137,AMS1,SKU-001,2026-09-05,26 +MV-00138,LON1,SKU-004,2026-09-05,-4 +MV-00139,AMS1,SKU-007,2026-09-06,-9 +MV-00140,BER2,SKU-005,2026-09-06,-4 +MV-00141,BER2,SKU-004,2026-09-06,-4 +MV-00142,LON1,SKU-003,2026-09-06,-6 +MV-00143,AMS1,SKU-003,2026-09-06,30 +MV-00144,LON1,SKU-005,2026-09-06,-1 +MV-00145,AMS1,SKU-007,2026-09-06,-11 +MV-00146,BER2,SKU-003,2026-09-06,24 +MV-00147,AMS1,SKU-005,2026-09-06,-6 +MV-00148,LON1,SKU-006,2026-09-06,40 +MV-00149,BER2,SKU-007,2026-09-06,18 +MV-00150,LON1,SKU-004,2026-09-07,34 +MV-00151,AMS1,SKU-002,2026-09-07,12 +MV-00152,AMS1,SKU-007,2026-09-07,-7 +MV-00153,AMS1,SKU-005,2026-09-07,17 +MV-00154,AMS1,SKU-003,2026-09-07,-12 +MV-00155,LON1,SKU-007,2026-09-07,-12 +MV-00156,BER2,SKU-003,2026-09-07,29 +MV-00157,LON1,SKU-003,2026-09-07,36 +MV-00158,LON1,SKU-007,2026-09-07,-13 +MV-00159,LON1,SKU-003,2026-09-07,-13 +MV-00160,LON1,SKU-001,2026-09-07,-10 +MV-00161,LON1,SKU-004,2026-09-07,11 +MV-00162,AMS1,SKU-006,2026-09-07,-7 +MV-00163,BER2,SKU-001,2026-09-07,-11 +MV-00164,LON1,SKU-004,2026-09-08,-1 +MV-00165,BER2,SKU-002,2026-09-08,-9 +MV-00166,LON1,SKU-002,2026-09-08,-2 +MV-00167,LON1,SKU-008,2026-09-08,12 +MV-00168,AMS1,SKU-008,2026-09-08,-7 +MV-00169,AMS1,SKU-008,2026-09-08,-5 +MV-00170,AMS1,SKU-004,2026-09-08,14 +MV-00171,BER2,SKU-005,2026-09-08,-12 +MV-00172,BER2,SKU-003,2026-09-08,11 +MV-00173,BER2,SKU-005,2026-09-08,-2 +MV-00174,LON1,SKU-004,2026-09-08,-5 +MV-00175,LON1,SKU-005,2026-09-08,-8 +MV-00176,AMS1,SKU-004,2026-09-08,12 +MV-00177,AMS1,SKU-005,2026-09-09,-14 +MV-00178,LON1,SKU-008,2026-09-09,-7 +MV-00179,AMS1,SKU-004,2026-09-09,12 +MV-00180,AMS1,SKU-005,2026-09-09,-3 +MV-00181,LON1,SKU-005,2026-09-09,-10 +MV-00182,BER2,SKU-007,2026-09-09,10 +MV-00183,BER2,SKU-008,2026-09-09,-12 +MV-00184,AMS1,SKU-007,2026-09-09,20 +MV-00185,AMS1,SKU-006,2026-09-09,34 +MV-00186,BER2,SKU-007,2026-09-09,39 +MV-00187,AMS1,SKU-001,2026-09-09,-5 +MV-00188,BER2,SKU-006,2026-09-09,22 +MV-00189,BER2,SKU-007,2026-09-10,-14 +MV-00190,AMS1,SKU-005,2026-09-10,36 +MV-00191,AMS1,SKU-005,2026-09-10,-6 +MV-00192,AMS1,SKU-006,2026-09-10,-7 +MV-00193,AMS1,SKU-007,2026-09-10,-9 +MV-00194,LON1,SKU-004,2026-09-10,-1 +MV-00195,BER2,SKU-003,2026-09-11,-5 +MV-00196,BER2,SKU-001,2026-09-11,-9 +MV-00197,AMS1,SKU-003,2026-09-11,-6 +MV-00198,BER2,SKU-005,2026-09-11,33 +MV-00199,BER2,SKU-008,2026-09-11,-7 +MV-00200,AMS1,SKU-003,2026-09-11,-2 +MV-00201,AMS1,SKU-008,2026-09-11,-8 +MV-00202,BER2,SKU-008,2026-09-11,-9 +MV-00203,AMS1,SKU-004,2026-09-11,20 +MV-00204,LON1,SKU-002,2026-09-11,21 +MV-00205,BER2,SKU-007,2026-09-12,-4 +MV-00206,BER2,SKU-005,2026-09-12,11 +MV-00207,BER2,SKU-005,2026-09-12,-6 +MV-00208,AMS1,SKU-004,2026-09-12,38 +MV-00209,AMS1,SKU-007,2026-09-12,-8 +MV-00210,BER2,SKU-005,2026-09-12,-14 +MV-00211,AMS1,SKU-003,2026-09-12,32 +MV-00212,BER2,SKU-008,2026-09-12,22 +MV-00213,LON1,SKU-008,2026-09-12,-4 +MV-00214,AMS1,SKU-004,2026-09-12,26 +MV-00215,LON1,SKU-002,2026-09-12,-12 +MV-00216,LON1,SKU-008,2026-09-12,34 +MV-00217,AMS1,SKU-003,2026-09-13,39 +MV-00218,AMS1,SKU-005,2026-09-13,-11 +MV-00219,BER2,SKU-007,2026-09-13,-2 +MV-00220,AMS1,SKU-002,2026-09-13,40 +MV-00221,LON1,SKU-004,2026-09-13,-4 +MV-00222,LON1,SKU-001,2026-09-13,19 +MV-00223,BER2,SKU-006,2026-09-14,-15 +MV-00224,AMS1,SKU-008,2026-09-14,-9 +MV-00225,AMS1,SKU-001,2026-09-14,-12 +MV-00226,LON1,SKU-005,2026-09-14,16 +MV-00227,BER2,SKU-007,2026-09-14,17 +MV-00228,LON1,SKU-007,2026-09-14,-4 +MV-00229,BER2,SKU-001,2026-09-14,-12 +MV-00230,BER2,SKU-006,2026-09-14,-4 +MV-00231,AMS1,SKU-005,2026-09-14,-9 +MV-00232,AMS1,SKU-004,2026-09-14,-4 +MV-00233,BER2,SKU-004,2026-09-14,17 +MV-00234,BER2,SKU-005,2026-09-14,29 +MV-00235,BER2,SKU-003,2026-09-14,-8 +MV-00236,LON1,SKU-001,2026-09-15,-3 +MV-00237,BER2,SKU-001,2026-09-15,29 +MV-00238,AMS1,SKU-007,2026-09-15,11 +MV-00239,AMS1,SKU-007,2026-09-15,-12 +MV-00240,BER2,SKU-002,2026-09-15,-15 +MV-00241,AMS1,SKU-006,2026-09-15,30 +MV-00242,LON1,SKU-008,2026-09-15,31 +MV-00243,LON1,SKU-007,2026-09-15,-6 +MV-00244,BER2,SKU-003,2026-09-15,12 +MV-00245,BER2,SKU-002,2026-09-15,-15 +MV-00246,AMS1,SKU-004,2026-09-15,-13 +MV-00247,BER2,SKU-007,2026-09-15,32 +MV-00248,AMS1,SKU-006,2026-09-16,-8 +MV-00249,AMS1,SKU-006,2026-09-16,-15 +MV-00250,BER2,SKU-001,2026-09-16,-4 +MV-00251,LON1,SKU-007,2026-09-16,11 +MV-00252,BER2,SKU-002,2026-09-16,-1 +MV-00253,BER2,SKU-004,2026-09-16,-15 +MV-00254,LON1,SKU-006,2026-09-16,-6 +MV-00255,LON1,SKU-001,2026-09-16,32 +MV-00256,LON1,SKU-006,2026-09-16,-5 +MV-00257,AMS1,SKU-002,2026-09-16,17 +MV-00258,AMS1,SKU-008,2026-09-16,-8 +MV-00259,BER2,SKU-005,2026-09-16,-14 +MV-00260,BER2,SKU-003,2026-09-16,-3 +MV-00261,LON1,SKU-005,2026-09-17,-13 +MV-00262,AMS1,SKU-004,2026-09-17,20 +MV-00263,BER2,SKU-006,2026-09-17,-10 +MV-00264,AMS1,SKU-004,2026-09-17,-3 +MV-00265,AMS1,SKU-007,2026-09-17,11 +MV-00266,BER2,SKU-006,2026-09-17,23 +MV-00267,AMS1,SKU-005,2026-09-18,-4 +MV-00268,AMS1,SKU-007,2026-09-18,-12 +MV-00269,BER2,SKU-003,2026-09-18,23 +MV-00270,BER2,SKU-004,2026-09-18,-2 +MV-00271,LON1,SKU-002,2026-09-18,-5 +MV-00272,BER2,SKU-005,2026-09-18,-6 +MV-00273,BER2,SKU-005,2026-09-18,17 +MV-00274,AMS1,SKU-004,2026-09-19,38 +MV-00275,LON1,SKU-004,2026-09-19,22 +MV-00276,LON1,SKU-002,2026-09-19,-1 +MV-00277,AMS1,SKU-001,2026-09-19,-14 +MV-00278,AMS1,SKU-008,2026-09-19,-1 +MV-00279,BER2,SKU-004,2026-09-19,16 +MV-00280,LON1,SKU-004,2026-09-19,-6 +MV-00281,AMS1,SKU-008,2026-09-20,-13 +MV-00282,LON1,SKU-001,2026-09-20,29 +MV-00283,LON1,SKU-006,2026-09-20,21 +MV-00284,BER2,SKU-003,2026-09-20,18 +MV-00285,AMS1,SKU-004,2026-09-20,-14 +MV-00286,BER2,SKU-007,2026-09-20,-3 +MV-00287,LON1,SKU-005,2026-09-20,11 +MV-00288,BER2,SKU-008,2026-09-20,13 +MV-00289,BER2,SKU-003,2026-09-20,-2 +MV-00290,LON1,SKU-003,2026-09-20,-5 +MV-00291,BER2,SKU-005,2026-09-20,-7 +MV-00292,AMS1,SKU-005,2026-09-20,-15 +MV-00293,BER2,SKU-007,2026-09-20,-14 +MV-00294,BER2,SKU-004,2026-09-20,-7 +MV-00295,AMS1,SKU-007,2026-09-21,-7 +MV-00296,AMS1,SKU-002,2026-09-21,-15 +MV-00297,BER2,SKU-008,2026-09-21,-3 +MV-00298,AMS1,SKU-001,2026-09-21,-11 +MV-00299,BER2,SKU-002,2026-09-21,-15 +MV-00300,BER2,SKU-003,2026-09-21,19 +MV-00301,AMS1,SKU-003,2026-09-21,-2 +MV-00302,BER2,SKU-008,2026-09-21,-13 +MV-00303,AMS1,SKU-005,2026-09-21,40 +MV-00304,BER2,SKU-006,2026-09-22,39 +MV-00305,LON1,SKU-007,2026-09-22,32 +MV-00306,LON1,SKU-003,2026-09-22,-14 +MV-00307,AMS1,SKU-007,2026-09-22,-4 +MV-00308,BER2,SKU-003,2026-09-22,-1 +MV-00309,BER2,SKU-003,2026-09-22,-2 +MV-00310,AMS1,SKU-004,2026-09-23,27 +MV-00311,LON1,SKU-001,2026-09-23,-6 +MV-00312,AMS1,SKU-007,2026-09-23,-9 +MV-00313,LON1,SKU-005,2026-09-23,-5 +MV-00314,LON1,SKU-004,2026-09-23,-11 +MV-00315,BER2,SKU-008,2026-09-23,-3 +MV-00316,AMS1,SKU-001,2026-09-23,-8 +MV-00317,BER2,SKU-004,2026-09-23,-9 +MV-00318,AMS1,SKU-008,2026-09-24,-2 +MV-00319,AMS1,SKU-006,2026-09-24,-2 +MV-00320,BER2,SKU-001,2026-09-24,14 +MV-00321,AMS1,SKU-006,2026-09-24,-9 +MV-00322,AMS1,SKU-001,2026-09-24,-15 +MV-00323,BER2,SKU-003,2026-09-24,12 +MV-00324,LON1,SKU-002,2026-09-24,38 +MV-00325,BER2,SKU-005,2026-09-24,-15 +MV-00326,AMS1,SKU-004,2026-09-24,21 +MV-00327,LON1,SKU-005,2026-09-24,38 +MV-00328,LON1,SKU-005,2026-09-24,-8 +MV-00329,AMS1,SKU-005,2026-09-24,-15 +MV-00330,AMS1,SKU-006,2026-09-25,-4 +MV-00331,AMS1,SKU-007,2026-09-25,39 +MV-00332,BER2,SKU-006,2026-09-25,-3 +MV-00333,BER2,SKU-002,2026-09-25,-1 +MV-00334,LON1,SKU-006,2026-09-25,-8 +MV-00335,LON1,SKU-002,2026-09-25,27 +MV-00336,LON1,SKU-007,2026-09-25,-6 +MV-00337,BER2,SKU-007,2026-09-25,-10 +MV-00338,AMS1,SKU-006,2026-09-25,12 +MV-00339,BER2,SKU-004,2026-09-25,33 +MV-00340,AMS1,SKU-005,2026-09-25,-5 +MV-00341,BER2,SKU-006,2026-09-25,-12 +MV-00342,AMS1,SKU-004,2026-09-25,29 +MV-00343,LON1,SKU-007,2026-09-25,-6 +MV-00344,AMS1,SKU-008,2026-09-26,30 +MV-00345,AMS1,SKU-001,2026-09-26,28 +MV-00346,BER2,SKU-005,2026-09-26,21 +MV-00347,LON1,SKU-004,2026-09-26,-5 +MV-00348,LON1,SKU-003,2026-09-26,29 +MV-00349,BER2,SKU-003,2026-09-26,39 +MV-00350,LON1,SKU-003,2026-09-27,-2 +MV-00351,LON1,SKU-003,2026-09-27,-13 +MV-00352,BER2,SKU-007,2026-09-27,-1 +MV-00353,AMS1,SKU-006,2026-09-27,-10 +MV-00354,BER2,SKU-008,2026-09-27,38 +MV-00355,AMS1,SKU-001,2026-09-27,10 +MV-00356,BER2,SKU-003,2026-09-27,11 +MV-00357,AMS1,SKU-001,2026-09-27,-11 +MV-00358,AMS1,SKU-003,2026-09-27,-9 +MV-00359,LON1,SKU-007,2026-09-28,-3 +MV-00360,LON1,SKU-005,2026-09-28,30 +MV-00361,AMS1,SKU-008,2026-09-28,-1 +MV-00362,BER2,SKU-007,2026-09-28,-8 +MV-00363,AMS1,SKU-008,2026-09-28,13 +MV-00364,BER2,SKU-004,2026-09-28,-2 +MV-00365,BER2,SKU-005,2026-09-28,-5 +MV-00366,LON1,SKU-007,2026-09-28,-15 +MV-00367,LON1,SKU-005,2026-09-28,39 +MV-00368,AMS1,SKU-002,2026-09-28,-1 +MV-00369,AMS1,SKU-005,2026-09-28,-14 +MV-00370,LON1,SKU-004,2026-09-28,-12 +MV-00371,BER2,SKU-004,2026-09-28,-6 +MV-00372,LON1,SKU-004,2026-09-28,-14 +MV-00373,BER2,SKU-008,2026-09-29,-12 +MV-00374,AMS1,SKU-001,2026-09-29,-12 +MV-00375,AMS1,SKU-005,2026-09-29,-7 +MV-00376,LON1,SKU-002,2026-09-29,-3 +MV-00377,AMS1,SKU-001,2026-09-29,13 +MV-00378,LON1,SKU-003,2026-09-29,14 +MV-00379,LON1,SKU-001,2026-09-29,14 +MV-00380,LON1,SKU-001,2026-09-29,-12 +MV-00381,AMS1,SKU-002,2026-09-29,-13 +MV-00382,BER2,SKU-004,2026-09-29,-14 +MV-00383,LON1,SKU-002,2026-09-29,-13 +MV-00384,LON1,SKU-007,2026-09-29,16 +MV-00385,AMS1,SKU-002,2026-09-29,40 +MV-00386,LON1,SKU-002,2026-09-29,-11 +MV-00387,BER2,SKU-002,2026-09-30,35 +MV-00388,LON1,SKU-004,2026-09-30,20 +MV-00389,BER2,SKU-005,2026-09-30,18 +MV-00390,BER2,SKU-001,2026-09-30,-6 +MV-00391,BER2,SKU-008,2026-09-30,-10 +MV-00392,LON1,SKU-001,2026-09-30,-1 +MV-00393,BER2,SKU-002,2026-09-30,32 +MV-00394,AMS1,SKU-004,2026-09-30,-14 +MV-00395,AMS1,SKU-005,2026-09-30,10 +MV-00396,LON1,SKU-004,2026-09-30,34 +MV-00397,AMS1,SKU-006,2026-10-01,-8 +MV-00398,LON1,SKU-003,2026-10-01,-10 +MV-00399,BER2,SKU-005,2026-10-01,-3 +MV-00400,BER2,SKU-004,2026-10-01,-4 +MV-00401,BER2,SKU-003,2026-10-01,30 +MV-00402,AMS1,SKU-008,2026-10-01,-12 +MV-00403,AMS1,SKU-006,2026-10-02,-7 +MV-00404,BER2,SKU-002,2026-10-02,-11 +MV-00405,AMS1,SKU-006,2026-10-02,18 +MV-00406,BER2,SKU-003,2026-10-02,-15 +MV-00407,LON1,SKU-004,2026-10-02,-3 +MV-00408,LON1,SKU-001,2026-10-02,20 +MV-00409,LON1,SKU-003,2026-10-02,-8 +MV-00410,LON1,SKU-006,2026-10-02,24 +MV-00411,LON1,SKU-005,2026-10-02,-3 +MV-00412,BER2,SKU-008,2026-10-02,-12 +MV-00413,AMS1,SKU-004,2026-10-02,34 +MV-00414,LON1,SKU-003,2026-10-02,-4 +MV-00415,LON1,SKU-006,2026-10-02,-6 +MV-00416,AMS1,SKU-004,2026-10-02,16 +MV-00417,LON1,SKU-002,2026-10-03,31 +MV-00418,AMS1,SKU-004,2026-10-03,-3 +MV-00419,BER2,SKU-005,2026-10-03,-4 +MV-00420,AMS1,SKU-002,2026-10-03,38 +MV-00421,BER2,SKU-008,2026-10-03,22 +MV-00422,BER2,SKU-004,2026-10-03,-7 +MV-00423,BER2,SKU-008,2026-10-03,18 +MV-00424,LON1,SKU-007,2026-10-03,17 +MV-00425,BER2,SKU-007,2026-10-03,-11 +MV-00426,LON1,SKU-004,2026-10-03,-11 diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/reorder_points.csv b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/reorder_points.csv new file mode 100644 index 0000000..65de70e --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/reorder_points.csv @@ -0,0 +1,9 @@ +sku,reorder_point +SKU-001,25 +SKU-002,55 +SKU-003,55 +SKU-004,45 +SKU-005,45 +SKU-006,25 +SKU-007,55 +SKU-008,35 diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/sales.csv b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/sales.csv new file mode 100644 index 0000000..da6cf15 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/data/sales.csv @@ -0,0 +1,6 @@ +sale_id,store_id,sale_date,amount +S-1,AMS1,2026-09-28,19.99 +S-2,AMS1,2026-09-28,5.50 +S-3,BER2,2026-09-28,42.00 +S-4,LON1,2026-09-29,12.75 +S-5,BER2,2026-09-29,8.25 diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/requirements.txt b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/requirements.txt new file mode 100644 index 0000000..bb77790 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/fixture/requirements.txt @@ -0,0 +1,6 @@ +# Production runs on our managed Airflow environment (Airflow 2.11.2, Python 3.12). +# Install with the official constraints file: +# pip install -r requirements.txt \ +# --constraint https://raw.githubusercontent.com/apache/airflow/constraints-2.11.2/constraints-3.12.txt +apache-airflow==2.11.2 +pandas==2.1.4 diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grade.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grade.py new file mode 100644 index 0000000..6869429 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grade.py @@ -0,0 +1,125 @@ +"""Grader for authoring-airflow2-inventory-snapshot (Airflow 2.11). + +Primary checks: the new DAG imports and runs in the pinned 2.11 env (Airflow 3 +idioms such as `airflow.sdk` or the standard provider do not exist there), +it is scheduled daily at 02:30 UTC with catchup off (2.11 defaults catchup to +True), and `airflow dags test` writes correct snapshot and low-stock files per +logical date, idempotently. +""" + +from __future__ import annotations + +import csv +import os +import shutil +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "inventory_snapshot" +DAY, NEIGHBOUR = "2026-09-28", "2026-09-14" +FIXTURE_DATA = CASE_DIR / "fixture" / "data" +# Sample movements plus boundary rows (stock exactly at the reorder point, a movement on the +# run date, one after it) that the agent never sees. +GRADER_MOVEMENTS = CASE_DIR / "grader_data" / "inventory_movements.csv" + + +def expected(day: str) -> tuple[list[tuple], list[tuple]]: + """(snapshot rows, low-stock rows) from the grader-owned movements; zero-stock rows dropped.""" + stock: dict[tuple[str, str], int] = defaultdict(int) + with GRADER_MOVEMENTS.open(newline="") as fh: + for r in csv.DictReader(fh): + if r["movement_date"] <= day: + stock[(r["warehouse_id"], r["sku"])] += int(r["qty_change"]) + with (FIXTURE_DATA / "reorder_points.csv").open(newline="") as fh: + points = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + snap = sorted((w, s, q) for (w, s), q in stock.items() if q != 0) + low = sorted((w, s, q, points[s]) for (w, s), q in stock.items() if q < points[s]) + return snap, low + + +def read(path: Path, cols: list[str], drop_zero: bool) -> list[tuple] | str: + if not path.exists(): + return f"missing {path.name}" + try: + with path.open(newline="") as fh: + rows = [] + for r in csv.DictReader(fh): + vals = [r[c].strip() for c in cols] + rows.append(tuple(v if i < 2 else int(float(v)) for i, v in enumerate(vals))) + except (KeyError, ValueError) as exc: + return f"unreadable {path.name}: {exc!r}" + return sorted(r for r in rows if not (drop_zero and r[2] == 0)) + + +def check_outputs(ws: Path, day: str) -> tuple[bool, str]: + snap_want, low_want = expected(day) + snap = read(ws / "output" / "inventory_snapshot" / f"{day}.csv", ["warehouse_id", "sku", "on_hand"], True) + low = read(ws / "output" / "low_stock" / f"{day}.csv", ["warehouse_id", "sku", "on_hand", "reorder_point"], False) + ok = snap == snap_want and low == low_want + if ok: + return True, "" + snap_shown = snap if isinstance(snap, str) else snap[:5] + return False, (f"{day}: snapshot expected {snap_want[:5]}... got {snap_shown}; " + f"low_stock expected {low_want} got {low}") + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly in Airflow 2.11", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is None: + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + return + + res, proc = g.probe_json(py, f''' + RESULT = scheduled_intervals(get_dag({DAG_ID!r}), "2026-10-01T00:00:00+00:00", n=3) + ''', ws) + stamps = [datetime.fromisoformat(r["run_after"]) for r in (res or [])] + at_0230 = len(stamps) == 3 and all( + s.utcoffset().total_seconds() == 0 and (s.hour, s.minute) == (2, 30) for s in stamps) and all( + (b - a).total_seconds() == 86400 for a, b in zip(stamps, stamps[1:])) + grader.primary("scheduled daily at 02:30 UTC", at_0230, str(res) if res is not None else proc.tail(20)) + grader.primary("catchup disabled (2.11 defaults to catchup=True)", dag["catchup"] is False, + f"catchup={dag['catchup']}") + + shutil.rmtree(ws / "output", ignore_errors=True) + shutil.rmtree(ws / "data", ignore_errors=True) + shutil.copytree(FIXTURE_DATA, ws / "data") + shutil.copyfile(GRADER_MOVEMENTS, ws / "data" / "inventory_movements.csv") + env = g.airflow_env(ws) + first_n = g.run_dags_test(py, ws, DAG_ID, NEIGHBOUR, env=env) + first = g.run_dags_test(py, ws, DAG_ID, DAY, env=env) if first_n.ok else first_n + grader.primary("airflow dags test succeeds", first_n.ok and first.ok, + "" if first_n.ok and first.ok else f"failed tasks: {first.failed_tasks()}\n{first.log[-2500:]}") + ok_d, det_d = check_outputs(ws, DAY) + ok_n, det_n = check_outputs(ws, NEIGHBOUR) + grader.primary("snapshot and low-stock files are correct for each logical date", + first.ok and ok_d and ok_n, det_d + det_n) + + second = g.run_dags_test(py, ws, DAG_ID, DAY, env=env) if first.ok else first + ok_d2, det_d2 = check_outputs(ws, DAY) + ok_n2, det_n2 = check_outputs(ws, NEIGHBOUR) + grader.primary("rerun is idempotent and keeps other dates", second.ok and ok_d2 and ok_n2, + f"second run ok={second.ok}; {det_d2}{det_n2}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grader_data/inventory_movements.csv b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grader_data/inventory_movements.csv new file mode 100644 index 0000000..54b64fe --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/grader_data/inventory_movements.csv @@ -0,0 +1,432 @@ +movement_id,warehouse_id,sku,movement_date,qty_change +MV-00001,AMS1,SKU-001,2026-08-25,66 +MV-00002,AMS1,SKU-002,2026-08-25,44 +MV-00003,AMS1,SKU-003,2026-08-25,75 +MV-00004,AMS1,SKU-004,2026-08-25,31 +MV-00005,AMS1,SKU-005,2026-08-25,34 +MV-00006,AMS1,SKU-006,2026-08-25,37 +MV-00007,AMS1,SKU-007,2026-08-25,71 +MV-00008,AMS1,SKU-008,2026-08-25,32 +MV-00009,BER2,SKU-001,2026-08-25,89 +MV-00010,BER2,SKU-002,2026-08-25,52 +MV-00011,BER2,SKU-003,2026-08-25,29 +MV-00012,BER2,SKU-004,2026-08-25,36 +MV-00013,BER2,SKU-005,2026-08-25,80 +MV-00014,BER2,SKU-006,2026-08-25,78 +MV-00015,BER2,SKU-007,2026-08-25,33 +MV-00016,BER2,SKU-008,2026-08-25,55 +MV-00017,LON1,SKU-001,2026-08-25,36 +MV-00018,LON1,SKU-002,2026-08-25,79 +MV-00019,LON1,SKU-003,2026-08-25,32 +MV-00020,LON1,SKU-004,2026-08-25,40 +MV-00021,LON1,SKU-005,2026-08-25,53 +MV-00022,LON1,SKU-006,2026-08-25,32 +MV-00023,LON1,SKU-007,2026-08-25,75 +MV-00024,LON1,SKU-008,2026-08-25,31 +MV-00025,AMS1,SKU-003,2026-08-26,14 +MV-00026,LON1,SKU-002,2026-08-26,-9 +MV-00027,LON1,SKU-003,2026-08-26,28 +MV-00028,LON1,SKU-004,2026-08-26,-9 +MV-00029,LON1,SKU-002,2026-08-26,-10 +MV-00030,AMS1,SKU-008,2026-08-26,-7 +MV-00031,BER2,SKU-008,2026-08-26,-8 +MV-00032,BER2,SKU-005,2026-08-26,15 +MV-00033,LON1,SKU-004,2026-08-26,19 +MV-00034,BER2,SKU-006,2026-08-27,-5 +MV-00035,LON1,SKU-002,2026-08-27,23 +MV-00036,AMS1,SKU-006,2026-08-27,25 +MV-00037,BER2,SKU-001,2026-08-27,-2 +MV-00038,LON1,SKU-006,2026-08-27,21 +MV-00039,LON1,SKU-008,2026-08-27,-8 +MV-00040,AMS1,SKU-002,2026-08-27,-8 +MV-00041,LON1,SKU-002,2026-08-27,32 +MV-00042,BER2,SKU-008,2026-08-27,22 +MV-00043,LON1,SKU-006,2026-08-27,24 +MV-00044,BER2,SKU-003,2026-08-27,-8 +MV-00045,AMS1,SKU-004,2026-08-27,-3 +MV-00046,LON1,SKU-004,2026-08-27,-15 +MV-00047,BER2,SKU-002,2026-08-27,22 +MV-00048,BER2,SKU-003,2026-08-28,-14 +MV-00049,LON1,SKU-005,2026-08-28,-6 +MV-00050,LON1,SKU-007,2026-08-28,-3 +MV-00051,AMS1,SKU-003,2026-08-28,31 +MV-00052,AMS1,SKU-001,2026-08-28,-10 +MV-00053,AMS1,SKU-005,2026-08-28,14 +MV-00054,BER2,SKU-006,2026-08-28,-6 +MV-00055,AMS1,SKU-001,2026-08-28,-14 +MV-00056,LON1,SKU-007,2026-08-28,-7 +MV-00057,AMS1,SKU-008,2026-08-28,-1 +MV-00058,AMS1,SKU-002,2026-08-28,-8 +MV-00059,AMS1,SKU-002,2026-08-28,11 +MV-00060,AMS1,SKU-001,2026-08-28,-9 +MV-00061,AMS1,SKU-006,2026-08-28,-2 +MV-00062,LON1,SKU-007,2026-08-29,18 +MV-00063,BER2,SKU-006,2026-08-29,-2 +MV-00064,BER2,SKU-008,2026-08-29,-5 +MV-00065,AMS1,SKU-003,2026-08-29,20 +MV-00066,LON1,SKU-005,2026-08-29,-12 +MV-00067,AMS1,SKU-001,2026-08-29,40 +MV-00068,LON1,SKU-006,2026-08-29,27 +MV-00069,AMS1,SKU-005,2026-08-29,-14 +MV-00070,AMS1,SKU-005,2026-08-29,-15 +MV-00071,BER2,SKU-004,2026-08-30,-13 +MV-00072,LON1,SKU-006,2026-08-30,-10 +MV-00073,AMS1,SKU-004,2026-08-30,-12 +MV-00074,AMS1,SKU-004,2026-08-30,-6 +MV-00075,LON1,SKU-001,2026-08-30,-13 +MV-00076,BER2,SKU-008,2026-08-30,32 +MV-00077,LON1,SKU-006,2026-08-30,-15 +MV-00078,LON1,SKU-006,2026-08-30,-6 +MV-00079,AMS1,SKU-002,2026-08-31,16 +MV-00080,BER2,SKU-004,2026-08-31,-15 +MV-00081,LON1,SKU-001,2026-08-31,-11 +MV-00082,BER2,SKU-002,2026-08-31,-2 +MV-00083,BER2,SKU-004,2026-08-31,-3 +MV-00084,BER2,SKU-006,2026-08-31,40 +MV-00085,LON1,SKU-007,2026-08-31,-12 +MV-00086,LON1,SKU-003,2026-09-01,14 +MV-00087,AMS1,SKU-003,2026-09-01,-8 +MV-00088,LON1,SKU-003,2026-09-01,-10 +MV-00089,BER2,SKU-006,2026-09-01,27 +MV-00090,AMS1,SKU-001,2026-09-01,33 +MV-00091,LON1,SKU-002,2026-09-01,-15 +MV-00092,AMS1,SKU-007,2026-09-01,-4 +MV-00093,AMS1,SKU-005,2026-09-02,26 +MV-00094,AMS1,SKU-006,2026-09-02,23 +MV-00095,AMS1,SKU-001,2026-09-02,-6 +MV-00096,BER2,SKU-007,2026-09-02,-15 +MV-00097,LON1,SKU-003,2026-09-02,-9 +MV-00098,LON1,SKU-001,2026-09-02,-11 +MV-00099,AMS1,SKU-001,2026-09-02,-3 +MV-00100,AMS1,SKU-003,2026-09-02,-12 +MV-00101,AMS1,SKU-001,2026-09-02,26 +MV-00102,LON1,SKU-008,2026-09-03,-2 +MV-00103,LON1,SKU-001,2026-09-03,18 +MV-00104,AMS1,SKU-002,2026-09-03,-9 +MV-00105,AMS1,SKU-002,2026-09-03,-10 +MV-00106,LON1,SKU-004,2026-09-03,-8 +MV-00107,LON1,SKU-008,2026-09-03,-4 +MV-00108,LON1,SKU-005,2026-09-03,-15 +MV-00109,AMS1,SKU-008,2026-09-03,13 +MV-00110,BER2,SKU-008,2026-09-03,31 +MV-00111,AMS1,SKU-007,2026-09-03,31 +MV-00112,BER2,SKU-002,2026-09-03,-3 +MV-00113,LON1,SKU-006,2026-09-03,38 +MV-00114,AMS1,SKU-008,2026-09-03,40 +MV-00115,AMS1,SKU-007,2026-09-03,-3 +MV-00116,AMS1,SKU-007,2026-09-04,-7 +MV-00117,BER2,SKU-007,2026-09-04,20 +MV-00118,AMS1,SKU-006,2026-09-04,27 +MV-00119,BER2,SKU-008,2026-09-04,-7 +MV-00120,BER2,SKU-005,2026-09-04,-2 +MV-00121,AMS1,SKU-004,2026-09-04,-2 +MV-00122,AMS1,SKU-005,2026-09-04,38 +MV-00123,AMS1,SKU-005,2026-09-04,-14 +MV-00124,BER2,SKU-005,2026-09-04,-9 +MV-00125,LON1,SKU-008,2026-09-05,-2 +MV-00126,BER2,SKU-001,2026-09-05,-3 +MV-00127,BER2,SKU-002,2026-09-05,10 +MV-00128,LON1,SKU-002,2026-09-05,-2 +MV-00129,LON1,SKU-004,2026-09-05,37 +MV-00130,AMS1,SKU-008,2026-09-05,27 +MV-00131,BER2,SKU-005,2026-09-05,-1 +MV-00132,LON1,SKU-004,2026-09-05,-3 +MV-00133,BER2,SKU-001,2026-09-05,39 +MV-00134,BER2,SKU-005,2026-09-05,-4 +MV-00135,BER2,SKU-008,2026-09-05,-3 +MV-00136,BER2,SKU-006,2026-09-05,-5 +MV-00137,AMS1,SKU-001,2026-09-05,26 +MV-00138,LON1,SKU-004,2026-09-05,-4 +MV-00139,AMS1,SKU-007,2026-09-06,-9 +MV-00140,BER2,SKU-005,2026-09-06,-4 +MV-00141,BER2,SKU-004,2026-09-06,-4 +MV-00142,LON1,SKU-003,2026-09-06,-6 +MV-00143,AMS1,SKU-003,2026-09-06,30 +MV-00144,LON1,SKU-005,2026-09-06,-1 +MV-00145,AMS1,SKU-007,2026-09-06,-11 +MV-00146,BER2,SKU-003,2026-09-06,24 +MV-00147,AMS1,SKU-005,2026-09-06,-6 +MV-00148,LON1,SKU-006,2026-09-06,40 +MV-00149,BER2,SKU-007,2026-09-06,18 +MV-00150,LON1,SKU-004,2026-09-07,34 +MV-00151,AMS1,SKU-002,2026-09-07,12 +MV-00152,AMS1,SKU-007,2026-09-07,-7 +MV-00153,AMS1,SKU-005,2026-09-07,17 +MV-00154,AMS1,SKU-003,2026-09-07,-12 +MV-00155,LON1,SKU-007,2026-09-07,-12 +MV-00156,BER2,SKU-003,2026-09-07,29 +MV-00157,LON1,SKU-003,2026-09-07,36 +MV-00158,LON1,SKU-007,2026-09-07,-13 +MV-00159,LON1,SKU-003,2026-09-07,-13 +MV-00160,LON1,SKU-001,2026-09-07,-10 +MV-00161,LON1,SKU-004,2026-09-07,11 +MV-00162,AMS1,SKU-006,2026-09-07,-7 +MV-00163,BER2,SKU-001,2026-09-07,-11 +MV-00164,LON1,SKU-004,2026-09-08,-1 +MV-00165,BER2,SKU-002,2026-09-08,-9 +MV-00166,LON1,SKU-002,2026-09-08,-2 +MV-00167,LON1,SKU-008,2026-09-08,12 +MV-00168,AMS1,SKU-008,2026-09-08,-7 +MV-00169,AMS1,SKU-008,2026-09-08,-5 +MV-00170,AMS1,SKU-004,2026-09-08,14 +MV-00171,BER2,SKU-005,2026-09-08,-12 +MV-00172,BER2,SKU-003,2026-09-08,11 +MV-00173,BER2,SKU-005,2026-09-08,-2 +MV-00174,LON1,SKU-004,2026-09-08,-5 +MV-00175,LON1,SKU-005,2026-09-08,-8 +MV-00176,AMS1,SKU-004,2026-09-08,12 +MV-00177,AMS1,SKU-005,2026-09-09,-14 +MV-00178,LON1,SKU-008,2026-09-09,-7 +MV-00179,AMS1,SKU-004,2026-09-09,12 +MV-00180,AMS1,SKU-005,2026-09-09,-3 +MV-00181,LON1,SKU-005,2026-09-09,-10 +MV-00182,BER2,SKU-007,2026-09-09,10 +MV-00183,BER2,SKU-008,2026-09-09,-12 +MV-00184,AMS1,SKU-007,2026-09-09,20 +MV-00185,AMS1,SKU-006,2026-09-09,34 +MV-00186,BER2,SKU-007,2026-09-09,39 +MV-00187,AMS1,SKU-001,2026-09-09,-5 +MV-00188,BER2,SKU-006,2026-09-09,22 +MV-00189,BER2,SKU-007,2026-09-10,-14 +MV-00190,AMS1,SKU-005,2026-09-10,36 +MV-00191,AMS1,SKU-005,2026-09-10,-6 +MV-00192,AMS1,SKU-006,2026-09-10,-7 +MV-00193,AMS1,SKU-007,2026-09-10,-9 +MV-00194,LON1,SKU-004,2026-09-10,-1 +MV-90001,OSL1,SKU-002,2026-09-10,55 +MV-00195,BER2,SKU-003,2026-09-11,-5 +MV-00196,BER2,SKU-001,2026-09-11,-9 +MV-00197,AMS1,SKU-003,2026-09-11,-6 +MV-00198,BER2,SKU-005,2026-09-11,33 +MV-00199,BER2,SKU-008,2026-09-11,-7 +MV-00200,AMS1,SKU-003,2026-09-11,-2 +MV-00201,AMS1,SKU-008,2026-09-11,-8 +MV-00202,BER2,SKU-008,2026-09-11,-9 +MV-00203,AMS1,SKU-004,2026-09-11,20 +MV-00204,LON1,SKU-002,2026-09-11,21 +MV-00205,BER2,SKU-007,2026-09-12,-4 +MV-00206,BER2,SKU-005,2026-09-12,11 +MV-00207,BER2,SKU-005,2026-09-12,-6 +MV-00208,AMS1,SKU-004,2026-09-12,38 +MV-00209,AMS1,SKU-007,2026-09-12,-8 +MV-00210,BER2,SKU-005,2026-09-12,-14 +MV-00211,AMS1,SKU-003,2026-09-12,32 +MV-00212,BER2,SKU-008,2026-09-12,22 +MV-00213,LON1,SKU-008,2026-09-12,-4 +MV-00214,AMS1,SKU-004,2026-09-12,26 +MV-00215,LON1,SKU-002,2026-09-12,-12 +MV-00216,LON1,SKU-008,2026-09-12,34 +MV-00217,AMS1,SKU-003,2026-09-13,39 +MV-00218,AMS1,SKU-005,2026-09-13,-11 +MV-00219,BER2,SKU-007,2026-09-13,-2 +MV-00220,AMS1,SKU-002,2026-09-13,40 +MV-00221,LON1,SKU-004,2026-09-13,-4 +MV-00222,LON1,SKU-001,2026-09-13,19 +MV-00223,BER2,SKU-006,2026-09-14,-15 +MV-00224,AMS1,SKU-008,2026-09-14,-9 +MV-00225,AMS1,SKU-001,2026-09-14,-12 +MV-00226,LON1,SKU-005,2026-09-14,16 +MV-00227,BER2,SKU-007,2026-09-14,17 +MV-00228,LON1,SKU-007,2026-09-14,-4 +MV-00229,BER2,SKU-001,2026-09-14,-12 +MV-00230,BER2,SKU-006,2026-09-14,-4 +MV-00231,AMS1,SKU-005,2026-09-14,-9 +MV-00232,AMS1,SKU-004,2026-09-14,-4 +MV-00233,BER2,SKU-004,2026-09-14,17 +MV-00234,BER2,SKU-005,2026-09-14,29 +MV-00235,BER2,SKU-003,2026-09-14,-8 +MV-00236,LON1,SKU-001,2026-09-15,-3 +MV-00237,BER2,SKU-001,2026-09-15,29 +MV-00238,AMS1,SKU-007,2026-09-15,11 +MV-00239,AMS1,SKU-007,2026-09-15,-12 +MV-00240,BER2,SKU-002,2026-09-15,-15 +MV-00241,AMS1,SKU-006,2026-09-15,30 +MV-00242,LON1,SKU-008,2026-09-15,31 +MV-00243,LON1,SKU-007,2026-09-15,-6 +MV-00244,BER2,SKU-003,2026-09-15,12 +MV-00245,BER2,SKU-002,2026-09-15,-15 +MV-00246,AMS1,SKU-004,2026-09-15,-13 +MV-00247,BER2,SKU-007,2026-09-15,32 +MV-00248,AMS1,SKU-006,2026-09-16,-8 +MV-00249,AMS1,SKU-006,2026-09-16,-15 +MV-00250,BER2,SKU-001,2026-09-16,-4 +MV-00251,LON1,SKU-007,2026-09-16,11 +MV-00252,BER2,SKU-002,2026-09-16,-1 +MV-00253,BER2,SKU-004,2026-09-16,-15 +MV-00254,LON1,SKU-006,2026-09-16,-6 +MV-00255,LON1,SKU-001,2026-09-16,32 +MV-00256,LON1,SKU-006,2026-09-16,-5 +MV-00257,AMS1,SKU-002,2026-09-16,17 +MV-00258,AMS1,SKU-008,2026-09-16,-8 +MV-00259,BER2,SKU-005,2026-09-16,-14 +MV-00260,BER2,SKU-003,2026-09-16,-3 +MV-00261,LON1,SKU-005,2026-09-17,-13 +MV-00262,AMS1,SKU-004,2026-09-17,20 +MV-00263,BER2,SKU-006,2026-09-17,-10 +MV-00264,AMS1,SKU-004,2026-09-17,-3 +MV-00265,AMS1,SKU-007,2026-09-17,11 +MV-00266,BER2,SKU-006,2026-09-17,23 +MV-00267,AMS1,SKU-005,2026-09-18,-4 +MV-00268,AMS1,SKU-007,2026-09-18,-12 +MV-00269,BER2,SKU-003,2026-09-18,23 +MV-00270,BER2,SKU-004,2026-09-18,-2 +MV-00271,LON1,SKU-002,2026-09-18,-5 +MV-00272,BER2,SKU-005,2026-09-18,-6 +MV-00273,BER2,SKU-005,2026-09-18,17 +MV-00274,AMS1,SKU-004,2026-09-19,38 +MV-00275,LON1,SKU-004,2026-09-19,22 +MV-00276,LON1,SKU-002,2026-09-19,-1 +MV-00277,AMS1,SKU-001,2026-09-19,-14 +MV-00278,AMS1,SKU-008,2026-09-19,-1 +MV-00279,BER2,SKU-004,2026-09-19,16 +MV-00280,LON1,SKU-004,2026-09-19,-6 +MV-00281,AMS1,SKU-008,2026-09-20,-13 +MV-00282,LON1,SKU-001,2026-09-20,29 +MV-00283,LON1,SKU-006,2026-09-20,21 +MV-00284,BER2,SKU-003,2026-09-20,18 +MV-00285,AMS1,SKU-004,2026-09-20,-14 +MV-00286,BER2,SKU-007,2026-09-20,-3 +MV-00287,LON1,SKU-005,2026-09-20,11 +MV-00288,BER2,SKU-008,2026-09-20,13 +MV-00289,BER2,SKU-003,2026-09-20,-2 +MV-00290,LON1,SKU-003,2026-09-20,-5 +MV-00291,BER2,SKU-005,2026-09-20,-7 +MV-00292,AMS1,SKU-005,2026-09-20,-15 +MV-00293,BER2,SKU-007,2026-09-20,-14 +MV-00294,BER2,SKU-004,2026-09-20,-7 +MV-90002,OSL1,SKU-002,2026-09-20,-10 +MV-00295,AMS1,SKU-007,2026-09-21,-7 +MV-00296,AMS1,SKU-002,2026-09-21,-15 +MV-00297,BER2,SKU-008,2026-09-21,-3 +MV-00298,AMS1,SKU-001,2026-09-21,-11 +MV-00299,BER2,SKU-002,2026-09-21,-15 +MV-00300,BER2,SKU-003,2026-09-21,19 +MV-00301,AMS1,SKU-003,2026-09-21,-2 +MV-00302,BER2,SKU-008,2026-09-21,-13 +MV-00303,AMS1,SKU-005,2026-09-21,40 +MV-00304,BER2,SKU-006,2026-09-22,39 +MV-00305,LON1,SKU-007,2026-09-22,32 +MV-00306,LON1,SKU-003,2026-09-22,-14 +MV-00307,AMS1,SKU-007,2026-09-22,-4 +MV-00308,BER2,SKU-003,2026-09-22,-1 +MV-00309,BER2,SKU-003,2026-09-22,-2 +MV-00310,AMS1,SKU-004,2026-09-23,27 +MV-00311,LON1,SKU-001,2026-09-23,-6 +MV-00312,AMS1,SKU-007,2026-09-23,-9 +MV-00313,LON1,SKU-005,2026-09-23,-5 +MV-00314,LON1,SKU-004,2026-09-23,-11 +MV-00315,BER2,SKU-008,2026-09-23,-3 +MV-00316,AMS1,SKU-001,2026-09-23,-8 +MV-00317,BER2,SKU-004,2026-09-23,-9 +MV-00318,AMS1,SKU-008,2026-09-24,-2 +MV-00319,AMS1,SKU-006,2026-09-24,-2 +MV-00320,BER2,SKU-001,2026-09-24,14 +MV-00321,AMS1,SKU-006,2026-09-24,-9 +MV-00322,AMS1,SKU-001,2026-09-24,-15 +MV-00323,BER2,SKU-003,2026-09-24,12 +MV-00324,LON1,SKU-002,2026-09-24,38 +MV-00325,BER2,SKU-005,2026-09-24,-15 +MV-00326,AMS1,SKU-004,2026-09-24,21 +MV-00327,LON1,SKU-005,2026-09-24,38 +MV-00328,LON1,SKU-005,2026-09-24,-8 +MV-00329,AMS1,SKU-005,2026-09-24,-15 +MV-00330,AMS1,SKU-006,2026-09-25,-4 +MV-00331,AMS1,SKU-007,2026-09-25,39 +MV-00332,BER2,SKU-006,2026-09-25,-3 +MV-00333,BER2,SKU-002,2026-09-25,-1 +MV-00334,LON1,SKU-006,2026-09-25,-8 +MV-00335,LON1,SKU-002,2026-09-25,27 +MV-00336,LON1,SKU-007,2026-09-25,-6 +MV-00337,BER2,SKU-007,2026-09-25,-10 +MV-00338,AMS1,SKU-006,2026-09-25,12 +MV-00339,BER2,SKU-004,2026-09-25,33 +MV-00340,AMS1,SKU-005,2026-09-25,-5 +MV-00341,BER2,SKU-006,2026-09-25,-12 +MV-00342,AMS1,SKU-004,2026-09-25,29 +MV-00343,LON1,SKU-007,2026-09-25,-6 +MV-00344,AMS1,SKU-008,2026-09-26,30 +MV-00345,AMS1,SKU-001,2026-09-26,28 +MV-00346,BER2,SKU-005,2026-09-26,21 +MV-00347,LON1,SKU-004,2026-09-26,-5 +MV-00348,LON1,SKU-003,2026-09-26,29 +MV-00349,BER2,SKU-003,2026-09-26,39 +MV-00350,LON1,SKU-003,2026-09-27,-2 +MV-00351,LON1,SKU-003,2026-09-27,-13 +MV-00352,BER2,SKU-007,2026-09-27,-1 +MV-00353,AMS1,SKU-006,2026-09-27,-10 +MV-00354,BER2,SKU-008,2026-09-27,38 +MV-00355,AMS1,SKU-001,2026-09-27,10 +MV-00356,BER2,SKU-003,2026-09-27,11 +MV-00357,AMS1,SKU-001,2026-09-27,-11 +MV-00358,AMS1,SKU-003,2026-09-27,-9 +MV-00359,LON1,SKU-007,2026-09-28,-3 +MV-00360,LON1,SKU-005,2026-09-28,30 +MV-00361,AMS1,SKU-008,2026-09-28,-1 +MV-00362,BER2,SKU-007,2026-09-28,-8 +MV-00363,AMS1,SKU-008,2026-09-28,13 +MV-00364,BER2,SKU-004,2026-09-28,-2 +MV-00365,BER2,SKU-005,2026-09-28,-5 +MV-00366,LON1,SKU-007,2026-09-28,-15 +MV-00367,LON1,SKU-005,2026-09-28,39 +MV-00368,AMS1,SKU-002,2026-09-28,-1 +MV-00369,AMS1,SKU-005,2026-09-28,-14 +MV-00370,LON1,SKU-004,2026-09-28,-12 +MV-00371,BER2,SKU-004,2026-09-28,-6 +MV-00372,LON1,SKU-004,2026-09-28,-14 +MV-90003,OSL1,SKU-002,2026-09-28,10 +MV-90004,OSL1,SKU-006,2026-09-28,25 +MV-00373,BER2,SKU-008,2026-09-29,-12 +MV-00374,AMS1,SKU-001,2026-09-29,-12 +MV-00375,AMS1,SKU-005,2026-09-29,-7 +MV-00376,LON1,SKU-002,2026-09-29,-3 +MV-00377,AMS1,SKU-001,2026-09-29,13 +MV-00378,LON1,SKU-003,2026-09-29,14 +MV-00379,LON1,SKU-001,2026-09-29,14 +MV-00380,LON1,SKU-001,2026-09-29,-12 +MV-00381,AMS1,SKU-002,2026-09-29,-13 +MV-00382,BER2,SKU-004,2026-09-29,-14 +MV-00383,LON1,SKU-002,2026-09-29,-13 +MV-00384,LON1,SKU-007,2026-09-29,16 +MV-00385,AMS1,SKU-002,2026-09-29,40 +MV-00386,LON1,SKU-002,2026-09-29,-11 +MV-90005,OSL1,SKU-008,2026-09-29,5 +MV-00387,BER2,SKU-002,2026-09-30,35 +MV-00388,LON1,SKU-004,2026-09-30,20 +MV-00389,BER2,SKU-005,2026-09-30,18 +MV-00390,BER2,SKU-001,2026-09-30,-6 +MV-00391,BER2,SKU-008,2026-09-30,-10 +MV-00392,LON1,SKU-001,2026-09-30,-1 +MV-00393,BER2,SKU-002,2026-09-30,32 +MV-00394,AMS1,SKU-004,2026-09-30,-14 +MV-00395,AMS1,SKU-005,2026-09-30,10 +MV-00396,LON1,SKU-004,2026-09-30,34 +MV-00397,AMS1,SKU-006,2026-10-01,-8 +MV-00398,LON1,SKU-003,2026-10-01,-10 +MV-00399,BER2,SKU-005,2026-10-01,-3 +MV-00400,BER2,SKU-004,2026-10-01,-4 +MV-00401,BER2,SKU-003,2026-10-01,30 +MV-00402,AMS1,SKU-008,2026-10-01,-12 +MV-00403,AMS1,SKU-006,2026-10-02,-7 +MV-00404,BER2,SKU-002,2026-10-02,-11 +MV-00405,AMS1,SKU-006,2026-10-02,18 +MV-00406,BER2,SKU-003,2026-10-02,-15 +MV-00407,LON1,SKU-004,2026-10-02,-3 +MV-00408,LON1,SKU-001,2026-10-02,20 +MV-00409,LON1,SKU-003,2026-10-02,-8 +MV-00410,LON1,SKU-006,2026-10-02,24 +MV-00411,LON1,SKU-005,2026-10-02,-3 +MV-00412,BER2,SKU-008,2026-10-02,-12 +MV-00413,AMS1,SKU-004,2026-10-02,34 +MV-00414,LON1,SKU-003,2026-10-02,-4 +MV-00415,LON1,SKU-006,2026-10-02,-6 +MV-00416,AMS1,SKU-004,2026-10-02,16 +MV-00417,LON1,SKU-002,2026-10-03,31 +MV-00418,AMS1,SKU-004,2026-10-03,-3 +MV-00419,BER2,SKU-005,2026-10-03,-4 +MV-00420,AMS1,SKU-002,2026-10-03,38 +MV-00421,BER2,SKU-008,2026-10-03,22 +MV-00422,BER2,SKU-004,2026-10-03,-7 +MV-00423,BER2,SKU-008,2026-10-03,18 +MV-00424,LON1,SKU-007,2026-10-03,17 +MV-00425,BER2,SKU-007,2026-10-03,-11 +MV-00426,LON1,SKU-004,2026-10-03,-11 diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/airflow3_sdk_imports/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/airflow3_sdk_imports/dags/inventory_snapshot.py new file mode 100644 index 0000000..0484762 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/airflow3_sdk_imports/dags/inventory_snapshot.py @@ -0,0 +1,65 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] <= ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) < point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/catchup_omitted/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/catchup_omitted/dags/inventory_snapshot.py new file mode 100644 index 0000000..5ed7b1b --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/catchup_omitted/dags/inventory_snapshot.py @@ -0,0 +1,64 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] <= ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) < point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/excludes_run_date_movements/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/excludes_run_date_movements/dags/inventory_snapshot.py new file mode 100644 index 0000000..adbb9d6 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/excludes_run_date_movements/dags/inventory_snapshot.py @@ -0,0 +1,65 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] < ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) < point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/low_stock_at_or_below_point/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/low_stock_at_or_below_point/dags/inventory_snapshot.py new file mode 100644 index 0000000..028f956 --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/low_stock_at_or_below_point/dags/inventory_snapshot.py @@ -0,0 +1,65 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] <= ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) <= point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/standard_provider_import/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/standard_provider_import/dags/inventory_snapshot.py new file mode 100644 index 0000000..b95da3b --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/mutants/standard_provider_import/dags/inventory_snapshot.py @@ -0,0 +1,65 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.providers.standard.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] <= ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) < point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-airflow2-inventory-snapshot/reference/dags/inventory_snapshot.py b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/reference/dags/inventory_snapshot.py new file mode 100644 index 0000000..557a9fe --- /dev/null +++ b/evals/airflow/cases/authoring-airflow2-inventory-snapshot/reference/dags/inventory_snapshot.py @@ -0,0 +1,65 @@ +"""End-of-day stock on hand per warehouse/SKU, plus a low-stock list (Airflow 2.11).""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def _write_csv(path: Path, header: list[str], rows: list[list]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + with tmp.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(header) + writer.writerows(rows) + tmp.replace(path) + + +def compute_snapshot(ds: str) -> str: + on_hand: dict[tuple[str, str], int] = defaultdict(int) + with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["movement_date"] <= ds: + on_hand[(row["warehouse_id"], row["sku"])] += int(row["qty_change"]) + out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand"], [[w, s, q] for (w, s), q in sorted(on_hand.items())]) + return str(out) + + +def compute_low_stock(ds: str, ti) -> str: + snapshot = Path(ti.xcom_pull(task_ids="build_snapshot")) + with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: + reorder = {r["sku"]: int(r["reorder_point"]) for r in csv.DictReader(fh)} + low = [] + with snapshot.open(newline="") as fh: + for r in csv.DictReader(fh): + point = reorder.get(r["sku"]) + if point is not None and int(r["on_hand"]) < point: + low.append([r["warehouse_id"], r["sku"], int(r["on_hand"]), point]) + out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" + _write_csv(out, ["warehouse_id", "sku", "on_hand", "reorder_point"], sorted(low)) + return str(out) + + +with DAG( + dag_id="inventory_snapshot", + schedule="30 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=compute_snapshot) + flag_low_stock = PythonOperator(task_id="flag_low_stock", python_callable=compute_low_stock) + build_snapshot >> flag_low_stock diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/alt_reference/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/alt_reference/dags/clickstream_page_stats.py new file mode 100644 index 0000000..84966db --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/alt_reference/dags/clickstream_page_stats.py @@ -0,0 +1,66 @@ +"""Clickstream page stats with pandas; intermediate data goes to run-scoped CSV files.""" + +from __future__ import annotations + +import os +import tempfile +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +ROOT = Path(__file__).resolve().parents[1] +SCRATCH = Path(os.environ.get("CLICKSTREAM_SCRATCH", tempfile.gettempdir())) / "clickstream_page_stats" + + +def _scratch(run_id: str) -> Path: + path = SCRATCH / run_id.replace(":", "_").replace("+", "_") + path.mkdir(parents=True, exist_ok=True) + return path + + +def extract_day(ds: str, run_id: str) -> str: + import pandas as pd + + events = pd.read_csv(ROOT / "data" / "clickstream" / f"{ds}.csv.gz", dtype={"user_id": "string"}) + events = events[(events["is_bot"] == 0) & events["user_id"].notna()] + path = _scratch(run_id) / "events.csv" + events[["user_id", "page", "load_ms"]].to_csv(path, index=False) + return str(path) + + +def transform_day(ds: str, run_id: str, ti) -> str: + import pandas as pd + + events = pd.read_csv(ti.xcom_pull(task_ids="extract")) + stats = ( + events.groupby("page") + .agg(views=("page", "size"), unique_users=("user_id", "nunique"), avg_load_ms=("load_ms", "mean")) + .reset_index() + .sort_values("page") + ) + stats["avg_load_ms"] = stats["avg_load_ms"].round(1) + path = _scratch(run_id) / "stats.csv" + stats.to_csv(path, index=False) + return str(path) + + +def load_day(ds: str, ti) -> None: + import shutil + + out = ROOT / "output" / "page_stats" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ti.xcom_pull(task_ids="transform"), out) + + +with DAG( + dag_id="clickstream_page_stats", + schedule="0 5 * * *", + start_date=datetime(2026, 9, 1), + catchup=False, +) as dag: + extract = PythonOperator(task_id="extract", python_callable=extract_day) + transform = PythonOperator(task_id="transform", python_callable=transform_day) + load = PythonOperator(task_id="load", python_callable=load_day) + extract >> transform >> load diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/case.yaml b/evals/airflow/cases/authoring-clickstream-claim-check/case.yaml new file mode 100644 index 0000000..ddaff90 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/case.yaml @@ -0,0 +1,36 @@ +id: authoring-clickstream-claim-check +area: authoring +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1500 +prompt: | + We want daily page-level stats from the web clickstream exports in + data/clickstream/. Please build a DAG `clickstream_page_stats` that runs + daily and, for the run's date, is split into three tasks: + + - extract: read that day's export and drop bot traffic (is_bot = 1) and + anonymous hits (empty user_id) + - transform: per page, compute views, unique_users and avg_load_ms (the mean + of load_ms, rounded to 1 decimal) + - load: write output/page_stats/.csv with the columns + page,views,unique_users,avg_load_ms, sorted by page + + The exports are about 20k events a day today, but marketing expects roughly + 50x that by next year once the new tracker is live. Reruns of a day should + just replace that day's file. We need to be able to rerun any day since + 2026-09-01. +description: | + XCom misuse (P6): passing a day's events between tasks through XCom (a + DataFrame converted to records/JSON) instead of a claim check (write the data + to storage, pass the path). Primary: DAG imports; extract/transform/load form + a chain of >= 3 tasks; `airflow dags test` for two days succeeds and writes + correct page stats; after the runs, the grader reads the XCom table in the + grading metadata DB (and the Variable table) and requires every value to be + <= 48 KB (the filtered + events are ~1 MB as JSON, per-page aggregates ~2 KB, so passing aggregates + via XCom is allowed); a rerun reproduces the output. + Mutants: extract returns df.to_dict("records"), extract returns + df.to_json(), Airflow-2 style `ti.xcom_pull()` without task_ids (returns + None in Airflow 3), events stashed in an Airflow Variable. diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/.gitignore b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/README.md b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/README.md new file mode 100644 index 0000000..11ae42e --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/README.md @@ -0,0 +1,12 @@ +# web-analytics-dags + +Airflow 3.3 DAGs for the web analytics team. + +- `dags/`: DAG files +- `data/clickstream/.csv.gz`: daily clickstream export from the web + tracker, one file per UTC day. Columns: event_id, user_id (empty for + anonymous hits), page, event_ts, load_ms, is_bot (1 = flagged by the bot filter). +- `data/marketing_spend.csv`: daily ad spend per channel +- `output/`: files loaded into the BI tool + +Run a DAG locally with `airflow dags test `. diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/dags/marketing_spend_daily.py b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/dags/marketing_spend_daily.py new file mode 100644 index 0000000..a76cc6a --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/dags/marketing_spend_daily.py @@ -0,0 +1,43 @@ +"""Daily marketing spend per channel for the BI tool.""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="30 4 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["marketing"], +) +def marketing_spend_daily(): + @task + def spend_by_channel(ds=None) -> dict[str, float]: + totals: dict[str, float] = {} + with (PROJECT_ROOT / "data" / "marketing_spend.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["spend_date"] == ds: + totals[row["channel"]] = totals.get(row["channel"], 0.0) + float(row["spend"]) + return totals + + @task + def publish(totals: dict[str, float], ds=None) -> None: + out = PROJECT_ROOT / "output" / "marketing_spend" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["channel", "spend"]) + for channel in sorted(totals): + writer.writerow([channel, round(totals[channel], 2)]) + + publish(spend_by_channel()) + + +marketing_spend_daily() diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-27.csv.gz b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-27.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ab0215b08beacce1cc9cafaaedd262e5916a2844 GIT binary patch literal 297531 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

TF;!w5+pc~7c% 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Ξ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

*Yds5phXp9mkebZ&D z&!1Nyc{!lZ5%YQ}46Tk&sP;C9xVz^j77 zK%PM|3r?eUcris2QlK4bPjuzq7Qm!b$_|cFtKYkaaTGu#kUQREa8ACUD=> zGrzE+>sa|ThfHb_-cN;iY4uzO-c*#Ukhv#BPC&eYvRvPFtIF5v3Jb??WShx?V0J{F861rp2Y@;R#Y3 zgX%ebtveZ{sF6A`ZS~#KcG{~Grp2XEJ*V3jV7yVyH_cIZ@1sP+bOIbKttKN6fM=dz zQQ+9!!lwX`H&EK>nHgPS7%Vb>w-6-Vv2yiqFM3OMyp?cCo%15%Q?m*6atbSP8S!XLsb8cB`Q__{0SF>g5qSu0lmayQJ9WK4NB zUl3@jDfI0(3;?{^uDse?8ID0w5r|b2Ox>~OE%A`x-0i=kX2H=ZrlpmSrDTNf2u$5j zM?N8j2G*ub@91yc`gi8NVqVz8_Fz`J($yTH1$9Y8Je3bnpJutR>*a(UppX<#A(i2Gc{caWf-g#M#(IwWjENCv*ZbB6P~$)=Y`E-`DZ)c6P!3b)nCF{ zL0#*+C-lx?lcIDp2Mg8YzH&^>g(6UEBP z(I6F4sA#=}@(8{PF(;!2kx;4mO&JZ6HYs|HuECX>DathI)h91LA~@sFn@EE4*|PHiGFO|laHdvCRR@CUd1 zX}-C=Ut}Iq;Hb*LH%~BBSTmz;6dvsi9@Q66+d+BVR@GlteX|_iKezELQ!HF|qGqvip6W2SWnHBh}M z?tXw$=Wf4Ll7lH&vutHA-<;bVH|B*GMX-XbA5i>%4szS z(m>jHc*o$Q%O#I}R1=$yU+Yg>kaNpOQ8JBERRQ|&;=^U!+?KZ_YM>0CJ-@dsGt%vJ z#b&4nSa4}f^^S;=MeO;x4>Hjh;mkuL7;o#Yr$I}j0>2I*| zng1nl|5Wx@@hhQL9Fzksj4SdU;W2=58In+*^aF-VORXc^nf1#iI3jtvPMrXje-O~k zpPW`mLyy(og1yJiQVf?lkhjX)iQ0(O-LIq{GWxs{DI~K!b8-=cXOsom3ml4ZZcxlG zGgFwk)TSv_NW-IAxMF(?Y2TUe+N%Z3cah17PRCtQX=8cP%okjAr*A3wX=x(3QF+P| z7F=|fms1`LD%FM2C(|#ap@pF97efq|pB$pw7MZis4i~nzsbo+`;KYf&c z7GUUZN~ifnnSMHCDBhrS0l-xw)@R~T2PG!h1TU^HQviMu>dC?k<4V|Cu6k433>YEP z3V$8iqS7y!sm11jAr)qsmF`R}z%*q}QJ~erqksY}cxFH0M)P0|(x=6iLsKs*|60v@ zK3)6DOFZBfG(WdN?QsZMZeGqd0BC4H-cuHfI({-(al2eHD2UC|FwI#rbC z2PF*ExDp<^B`1ApS`uhx9&uT4To8lg|DNUeHao5!o;aE!K5w85GjZ;IR=q&XT}N7= zvQyyd5Y7~Ea$`pIe0pS9Vuqh`FYt1;?myc>5JFs{J>)_rT5UBCRI=)k+78!4ME%8? zp@02vBOiakGnX=un7@?#;CsDburRz`=Q9_J&EYYOo)>tzF{60D=l@934pmYEmpk-( zFEFZIJi@^V5VxKKxOJq^`crneI@l~D zJ^FOCYQ)DKekBZ0=g7Csf>;FV9>Z7&M2jII&$d<=VtUDNMSW*D6h5y|^M&JxJt(`3 zn>(}j2BRpYg$84`g;+GAY&e830|~Sqq+1>8qJH6tF_=(sM!PyNx{}tDu#i)ErWbRp zf#Yb5d@kP!4o4hLd3Y`mr)1muCA-IE3eokR)5}oJ>zKmtqA*p;5Qv&51uqyd%$8E<5WKrg(ryZ~jMRuzuKpzyn6)~jL!)Jh0G~KbYsHI{s zO1PQGL{p=b*I!nZ!LR#KI980B%Qc_VE^Gb$<^WhXq^7ssN4D{3YxlAmXak>ZwG=SXYzpyvk|;&JBs(&q%a3&3U$lg^-`| zyb$yV1kFGRT)=dr$mb~(sFuxV z&bLzEj4w3jHpyMg&oY0lNGG4_M(=ye4KA*Z3l5U)Ztj`e)0eiQxI~FV9_hQU2Y7h* zx7p(csF}|jPz){E1^)gu6)Lh)hU-H)yaD7!N$t~|rgbpVgNej0@F|>WTxeCSm zXd@7fdT#J8MG^Y99v&p@7Frn>vRyaJ{U9gWz;@RfmtQMdZVi_AeiR$n?pi4pwUqjR z@Qm-RKw4F8$`Xny$<|%#Crnu+3cB-Z6mF|yT_C^n~Erv|H0#zA6}T-oENSEZhdGc zyF7njgO0l{;db=3K?p+3tw?U=nHF-$Z!>(n`;`^Mt<;WKc8Kq*8QvM7xr#bxEc$72 z+rB8+VB?mZPWuR9CEDkQbEzT+g2yG|y8!^>KsDRo{o`f9xl4<3;F);ICLf*WK7!<(dy2V zo8@;(ukKN$4TQJXnvWdBuc4^FZ1|d@WcmlxUQyTSFJ z4^~q}?c~&7KJ3=!xzH#VoOVk^3PPq0&xPKzT~vzDVMh2R+!Ppe^GQ^sR*qm|dGhLp zZ|)p?Ar4m~poBN9L#ZeQ?CXx`7(fMp!GCots{1)J+J)}INO@!m3t_d~Tb@YC)5rwj z;DdSYNF86H*jMUh^VU?m<5g+noVQp^S`g+ATk%{a)Scc+9S{qcJL4jxiTmT>sc5cv zzj%8};fCG9+-Z(4D?(F|gyX*h19NA;_a(}?ql=(Y!t+2dQNOu0$eV{LU|0#7 zR6Yt9oE%;&v^(VaQ02D?(Mz}jBsLzbH#NNGpBUk-5Iguz$*Cw=^2T?epfbIqZ zxaX79k?!YX4*U3AKP_B<#I#|&uy)UVs>F~&Jw(F+10!9DVEgZBQDwzv3w{5xPP(K}gUSJw;Tt@T zHi)`G;`8{{F}J6O0&&AhH&&6)fv-6J^3&IDFJoc$Bdjh^3UuG$XF?Ml3}c^cQMRaZ z=CD#XmT3F-@}n-AeG_@bd!vYHS$V)3&!%NX#8NiAx;-Vby(77G6dvIT5O<~EJH47y zY0OG_3Qq#0-O^IB6wE~QgqUFigqg0wg^6gGn}zO#1HxclFOsNlU>O$-RS96;@KaxE z4&i>PC_bQ}z%?*ZK8dD+bSPNznN!Z`P3*v6fJbl`A%SaUG=ieo% zl=9Q89NcOdg)h6pkP1w7)!_CGFKO&3q~dNqMFpJQHB)V+Wj~ilwf`)%oceYVoCu=q6p`LeMdfda5R+7NXSSeYCeF5ARacU6ojV45~$m90@h`N&ZIyLKiCVUJxb8LA$TB4|9g}w-n^v zwbPcGfYmFFkNrDCAiuQCK0#SXD#NQg{DI)Ute(5p7W>%ep9_$(!PFp!F27WuCVp^Y zsQ~B=dKY>}%V_w7_qkGzl|TTiGclv%-!fLFiv-g5{ zHWDz~|Bc}BM!WsD@>ptw>Ye4OfW{j#n_fH(Gj87;ek$bj(qX^p{rXAJ2p0kqGq4Za z+cIAo<>e$VR2adFyWf(n;e)utyN#r@i=~J<460RJD}D=w|H5dgSGe{ZnCF)87!Z{f z8l9JIp9Oz=_3~#SxS5MDRy0DE6qGr1Wv`+_WC2nIgUNUpdGjen}c%*8D;RdXXiT8ar1UO==40R)Uw=Q!Oh&4y3Z= z;a}2*rn;_+5s^S7YJIc9uSGgQ$*?Z5ds+DS_wx4?j9w+RpRk;vUA(X{wm)JN@8#)> z3qE{IVg(R?rdibWqc?A{{L&c;nKs|XGW8uOtDF6t;a5Vj7LJ!EX~g{V)$q%$+`n9$ z;}Kf4mDF?_|5HKaZIl{GY3v{d^V9F7=EqPk33R=gk5utD6fV0hs6?JFg9C?sA^)nG zDZ`+Pv$_b`h0HX<6&otQO^Dd8@EEg+~O63&NcsP zgUcJ|`7gzN)e@h@|MIV*>9*0_hHeCkq^gMAHPLD}I@b6SiKL--&F>e0es>=i(3rpv zQz@)li5wD2Au;;d0(1D8;PS?-_a|5)ZIgQSgyV4hnR%QuZDsmGrmb|#&@^#MS&{a~ z5K0bj&gkzwvAv_FVDl6<4xqo&&%mA&70KQWw=7TE5=lAsP!-D=9Osf0^2q z!#=YiGT}S}P9Z_ogm~u%sk^Zvbexv$>S|V=-~fU28*B<5o3Q3-t2dJ`q6t{=NL|#_ zi<;@7`BJK|)L(odN_7CGzPliJH_W+j0_}-YZTg!^1$js%Upcg-)%`SFQumv=TaM5* zgcMB*x71W^%E;-#~auq1nIcKgxGrSG^s+GAOL%-_*Yx8VA``?z?Ncs z6bkp0VOBP7;TUFFQQHnb7F!LKThJbOpgL%BD8KSmmkBRiBr^0j_y5PtU7qyn;abou&2@>{W6&GPmp@c3 za3T+}5V}Ljn^_Qrh46*RYx#l{H}U8W zJ0;k?BaK@Xa~Ja~mE69+r`*sZ5ntjnIy7ycW6|$<3i|Tc`x5+M)pcmD2=)!T?3}Yz zET@OVd!nc>U&=WtSf^!86jzoLVBa9(dF;>St8%Iop7}RjtsI8*?W9u@TShs17#H=m+amf4}^O6IGZJK7h7^uFTjx=q>7PW5le;Sb9D3Ex1fT^ePBlM35 zNN?af&~vgUY_kzfi-oz5i$}Ug>MCT|*1oq2;QLmeH7n2>^m`Shjv8#MvNH zH&GSj?6AEL8!DA!8iOp;%p5;UBr|JRDF7-5-nIlc_E(#jTXQ+f2(Sg#>W6`nijuyW zBu`Eoj93iAp&^(9KE$YcHHFz*XoKOuyVEd*qFcQ3`aSAKfOa?YC00i6i8e!_sR6#b z61yZ)5|(^IwBiLJu#!p&Tzxw9|t#u!XlR|}5mILd6!0&DI;%F|dkWRZo z5wb9O-1TzS3XxSJ$3wpX?}h-kz8<2PT7-!XS)$r3m#?WoV#a=mMB9dsHx;@l6V(cv z{m}`IuMjz(wgUwQk)OlW-xRQW1Aq0g#Y=aBGAVF1X`8=lNrx*C6-KfbZS%Ct=#03P z;<^#u6;OMp$6A=qGr%3Mr>{$Zyq^ImW)MQ5zEW71dP$kD^{64l07 z7jA<&Y4~^_VzcATC-FslZ^UQ%)0PXS)R!PymA>k(9cZOoEP^k!)Dzx7)lN7J${4P^ z7vptlk&2~Pt*{bm>yoFa=zLW3j{?xj+;URc}@i-9ImNljVck2Qf5m_qrgjMK)gFJ()S^G9!AUI zaUch2oT+9ix7HH4`R@-NQuoDd}+zxs|>ziPiDiJuO!Y(2^HxHnhhZswD~f_oz| zO@AMR`Sf0f1+s`kKw<@v`5v{$nOGTqXG zOm2JFf??Miw9U_fwMEzSYJTen-B*jSLqFNz)VqCy#~}Jne_Q5T7NV!AmwSH}La0A( z-oV0T-_dH4*bIAXdPOBfX&!Mn5nRFZ)Br;NQ|N2e80a^ytt9Z+ufTuk!Dp_u^?EEx9La0T|XHQY&yV;k!hS;_|q z5>G^I?R60Y+OSAzH*RG^a(9X+NlinxxwY_X?S&baezt)nyk@vHsD)>41GRK_Q=)J< z_NS?9Zxsq5yG-#Jw-PDUxydSEZ*37nZ3D`0pzgi{WUTS3;r@X1TV^K42g0C=W$)GV zSx9h*$t>SR$6d85+S9+Q0EUT6%BZU#v@`n&CByT9g6m3si!8O59bqAaZx^ww+!#Hk zR`pK<)L)tDWC02r>VKcVrPC%2p3JIf@nJxs>Gr%DUXZ?}p6$q{0wNjyJ`}{9>sDEf zY|sjYN3qegtvt0@)Dt@9TK?OBr>}7M6HC7GMbJE)%rJf^>z0Mpv3s*LZGXkhhO|Ia z-yaPILnEt}2%h}KyS`{7FSDHnKFH><5K$-jkQu%KO;I;5b!Qkqh%-h;(&%ilECD(L z<5w+Vj_dl@FKdN2W}A~t>ej9fn!29`VIRZvA%tqbJb@TyZoEo(^7~zX)wjceK91&b z?25d$Hg7iL&3s~qi=Cnh^M^3ndBt~i1f2(uV5$I!JE#6v@-0?GVR(-xs^vXacqV1j zS8T~)VS}mWxWyy^wV~;C>$R8_5#KzX%@@VTY<^aPYQ;ywIH`NSbT7# z7`KL7xS9#HALcvw_YA^Q`1intYg!5`9|`08;(_xDTzHo}Q~+Tpq2tMoJLR4CAJ}gmhYQM0;UuHd1Mu8|g!PSM-!qeWrB&?@T z6^2dGnxG=6t(TF+NkczSi!WX)OdcBDir3Z+aEvP%MoGEhm+~~^^9)pm3QymDIZlT9 zD5@Qh?~*D2;NXw-iZNs3p^!T+E4aNYeFm~Ae6w-GhwuOa{Ho9D!FH&a#&CO-ssi=Z-TeG@ zvt`qVpJn*dq!#K~wg%yDB%19_CHRI`}Efu~FF|+3&{E-RR zp5f1XPnGB?`(hi{iDB{IdkJQ5G%V)x5%+1t{YM0Qhram6w?)NLO9_t54Jv(KygyapvUb}L`|qKfufs~ck);1>7FM~!N|^bU zM0_P>6xnQd_%j^d4lN+|_0470tdhWXuMIPTwR0M*SSIiz9g}KDb=&EJ6m@K#N;UfX zu^sl$g)Z&~k`I7C(|`3HLVYR!Ul@}{S-=U=`~Or}I}GN_M7U$7#5?Rw0DJf9m%QbQ z>5w);ZY84HRzJ!vnFxuZg%Sd-rzeI=j-;=9Ny(G!a*Wvx=J~*YPKR zJewSekuL@=Gt|~#{H`%?_WS)?*!iajptqfY-lj;Fgr$YM2|tJ3Qb0=St!j_7!!8Ot zhfufDlOEnJvUGStM+NOSa5G+9umW-}4%oplFf2RUuJ^&WL64O;hDD+ALd&Z2nWJ0P#6B&xd~2yyk`i!X28hO&6$#0peb zf;vvG3J;4;3mwifAzPM%28(FS96cquZkst~h1U3TVmNFNVEHz$#(%o+$n=jMT$*Y= zzWq!tlfvv9;Nw38SU8L{MjZZUO=Ub3{<`*JQNCO9 z!Uh00#6isW*bf1do^jQH-}1_>Ak~{#|41))Xa%XVBOirpj49mN{qU{{xfhz4*ui|qUnjC>O=KE5!ur|tN+2b2V7}X$+t(hLjy^xSg zD?+4=FjNE4p&^-#XSpPEi5-DXo_#yg%&T{fH`WioDAf-B{d-Ggn`u&nz^MrHVz25G z8sEVXwkhiHxe+^TaTO51ukz>_#l;4W2r101tdMW!kmytz1?zU@7n-W1@feB~HT~82y+%X-QZ zM-5v&-rE4%imT(vq*yHX)hnroAE<9gm{|4ah7DJk5njn=>5jQq$ox_ztM7GMvt*l` z4ye>@+22CB+yK{x0{v+yqh^jOz4hzXPsWuyG{Hyg)ARcewpl6?UEo^7q(d}B>lSj8 zgFaIVla7o(|J1t;5PJ&xAa+}tp6L$@@N;L~Ha%+M}pw$j#-eClV2 z4Unxch2Fq2B5wOLe5Apeq11X|S@Z$kc@TcJ0q`x9q)$OJTs6B#RN8>pex&xFgnB_L zr@q2U{&`j;Lj^Fi;T!8esR5(RG~h!D;A+0Oej8QDw$4VET!CT&^Po6vM$8?laeGty zIW=AK09*qxcZ`{vP^ix3k^85%9O~Yl*(;)2Cybu`D*)Vb<0}fY+QJ@=1U8A}$_8MiSx}NE&a}xt#N*8rQ9m#R!Z}p4 zKy4|=eqRJ~3l-TVtb|W|eQ362@=VZdix=i!QKncejXW)d>1W#=*i~)9@v^+(Z}dHL3~VE;)WfJ#Zu+POv#jyr;SU59*Yb~GYC+D*-VZC`@$GZM zU&3-hR+Odar#cO=?HrjmhlTPe8Pjvm8WPz0nt70~7^$lDf&X5C+eX9vrWlJEdUFFbK>3!@V0Qjjn1kJDVdHg%6d(ExjiFEhH!na)DY`Ft_L+Yx_(7<(wbfQs4$7#gi{&I)Ge> z2|dB-1*Ro|58si&7EIpw2>@2a?{W9NU@z~c`gt>@FZg`>p*`+q(~t+T6)o*sd1PZ# zM-bcc+`uOrM_?f^&8uGsFBncUGm4JsG~OZTt$W$O==b#Y5RMua1DL-@uV`+$xiX;8kr%sTj5nl*7#zL)LI^V&@ooNVIQO_`)IjkM72&F-u(Si9U5Obtjd} z0EjG|J-8ZnST(TUt)9wM@nqwtN~J5@TV*tp<|+y;M_F|q`shG=Mce%D({sEwpXgsz1_h9Vyeff>8f&f%KC%=C_)U-}lG+d{AGhVG@KdP6`N$7BD zq3q27y7@8$+vX=J9JB(>cytTFyYynT^=^L@iw(2-4)}M^nxBGl?8*?ShbI6}-koX4 zHPab6bG{6PDMJjIe{wIS8rAy8w@n@14DwkW&iMMBtxXS6_Ks=v8W+`Df$=ssKdA3; zWeB9GXFgqn+fd|&cMD<_T1F8>9x#dxsZ;dV;>6_*T$P zy<7ZW1cvSoF!6eB5hffC!GaxdZ}7E#F?!6wviXTkhYbU_enHSXto$h;=pA_P?(I^4 z6qrm4fcA0XD7bwWRDOPmAj2y->=fzAeaAW%gK8N7CBtoLe9f9OyV;?R}br zey6&yuIw~cB-+26aA2suEqVbz#YQdOlfO?Za1VXB)QbZz7lr0+LO<^jvB#08qgB>S zg>ZaLvxYC1wy#Et^@h@w`9@3>_pZudfmOMpW^jp}$h-9zb@!nX@aBe!P z2fCslVeHg_^Ts6fC$1=(U;Q3J$q2gzP6@26E-jc@hC&)RuLxwHJR^cHY7JKM9r$ka zntgiWlBInF%ZrYDwe?g#7Y#qrc+!xD+2OHJdS+2*VK)i}dhJqIRJV@NF3#ZJW-{0u z^=*bOIRL4<3OzHo`yT%;q!M!uA@Bdx4bcrT*6j(C4k&{H8NP4K7;_L=6{m^`wb0;g zGd8TLwFZoQf(!XTpjwy$wfE_z!KA(e9pxPz@N#ws)jZOV2f*B!r&GYbP4DUQRn92p zhMDqNhDN`sIZg|ggpbQS&;jw86PR$WNE|?Pxt0qML@@B8_$#45+kCSLHr;$^OGYs^ z-GP*V7Dy{U=J2QCZK*Y9cvRSE(eefA04X=b(6o1Vp|q!vU_QXi<(-a>s>U5DU(L(w z1{`-+P4Q8N+oOtG-7*Z0$|c0QVOCs27WDv-8ziz%^Hec5l5`mlbPiB*OHWxPYEgw- zQ^L0dPHtZ*J%BX!LkaQ$SsTD}=P`I?XaP}F!V<2A{D6|ib|SJxPTL-?g>hrA7wKU~ z3Ky3VRvKw)U%W8{9#CzN7xWD_1wCD)fNfalk$+A30JHF0b*$c-b%vFaI&t*GQB**? z^zE_nVQ#y%+?;3ujB?2884RXf+qvgU0fUJGS70cLtAIo%8qkT8mX91h3ov!Hi9Huu zk_fNIu0JFOV@kbR=1kS0=ZhTYfiN|-Fn<$ew1DIu{=K%#JA|Y^w`xnV4Md2c;BDnG zE`fdDSneGbVo)^FuU#QneW@kG>+caLcw1WEm-y2%zMq4C>Y(mMe*k_H!jSF6VWrkE zA)NcEE2@^*7a3JnH-`k`@3vZAr!}p;O zu3E09nyU^&3a;^z8~JGYwx;lvZlYuRCkZC+4sMiBoAkOBlGU!|@0txk0yBq!HE~Ws zh8;FG*zN9o6UI5Jc0qwiIsoe`Gy1qoO(p#3IzQJQ5!8LfmSa{FbD9Sn7<%0o1%4TZ zdwGiGK-5~GZ9IL58_L5G|2o}g_R(0X{3Pl+PIQYPa$Pwi=_mN$?#u1bj9ynSPJQMS zr7Zowl^rgICEIYMubPMj^JV(z#-8ezHbmee)$EbeFbQFl*2I~u@uYpV7Vq#Wbr;U~5Ok!!(3 zqrPcgx<`e?iJ@%XBB9+Ub+tNlYP~RO_?eg;t+OV3>T$TAnFC{3aCtk9Pg^%dX}lp| zzx32&SD*vL@UYOA{$90-S->UjTy5F4Ba~63TS>nZ;#Wtg)NfKok$*iw;1x&U6M9Vs zL?Mm(c{CE!T|tABGf$8AjR~pRN^K3qlz*18Jn4&lpXHMev#T=%!~@3oLWQ0iMQ%C!`UDnBjD}|WPb#UG`^%fUH1o=yBuiW&jM0NV`_F8 zSE8NH+1nyrGbwNNwhd!Lgp>+QwXVj76yO^GML@d0;JlF#{fs#jU`sWX@!9PcNdK}+ z`Fywv(hom@nBeZNm8UF8VFyLEAL31*y`frqh38g`w429oW-(2DYJcMBhJH*uLEmkA zfV~Qj={cdniB(`&Um#YRu7brl_yE67YMB*1Z?@*YQjX9R^I$Cbv0N|jDG^^ zZHq4gbTiSYfekw?T&=H5l0n}l1@%1BY3XV*mwPE>l_DuR%!sYhV>P6lo_(+))h@I(9P?&pRRR9!v7UI!*=0^ii<;E2JKF%k&j8c*+7 zhJ<}m_?7i#l>KPEmqs`WC!~d6i3w88@dbFvBl^w1GvMjYxswJ(3}@A@EnZK{T;=?o z3MJMLRzg=j&1?eP4MN&C4;86V6t`)3>Wa0~`{SwKh)JdvZYHA2ISm=ZDczN8;XgZA zGRS`W#v~Li6PiVQOMvb!PPd|>vG0m4G>?2vg?DRI$jo>7u3;P@GI>^TqjrB$No6nw zkwdr=jJYWbm?`SyB85P(@tL*%xI@RxHTseYsd%r_2~1aI`bRr50crHx@x}YB=FUxd zeomk2hY?KpC&JTast6|-s;fm?_~t6C#W-s&0j)N*`ts`arA#O!`4C`KkMb%R~yeqwA(Yv~Y)FfBW6woA0O0GF^A3x8t6Tb+sqbPS-w+Yl5o zTYvFE_{Dt$;hf@QY)1Z{mqd1k+PeCQ&IF(vhaFEOSWH~E#y5of3W^lhOJs!N&r;3% z4+`07eXUFE9(;p>73A!HcHLPRA;YLW!fX=`4Wr*oS<5%#K73WMXy~`xOnL95BIoeZ z!J%O{z7TF?4xltjzpnk%Z6^=O4xE*XM`)+fbp0vz#PkTR4mFml3hnYLc@P6E8pH@R zoGX2%%$cgFi5{3944!~=l?D5B-my`mXE;p|bDP%{KV7M;?w}y8pJ3<~Ka%FB^iEn~ zo!-r`((;RPjgSyu@PZZgTlRo!cVGE*vo-UW@{ZyYFfJ9o>BE=pF*U42URpi-&`SY= zJgE`xDo|Vn%g)nfg0D1D{Fx?8D;KOxS|}YehU;k)ExEOa*IC;WwLlNmILsJ=t=MZ& zSj~SYC@Lmgbb|-_dP_GN7<~RafE@$niWYGH>QXGHQh^D;1ur@88`JVaFWj2fH3 z-jrp6xEs;Br-6*7tvy3s=d$v597{dbImoUZsIhbXF*47ajW(+~RO_BNKMmgv?bVZ1 zW?`ozJb$owcTG8gFbB}%({nH;>>6h3O5s>$D1IL^rENm#6E@LM%qKjcCbDO(IN+B(p z9Yf_AB{t#Epu?OKEuq7mp|*f1!en=!Tlk^`OC*4XHU#)?$>)XU!J}_jc&Rrm4n0co zWRo?3v>YsGS3yAe%zd=01`Vkc#ow1zAU*P=Tt0X(>6qBRG0WJzE8WvXyj5XbGrgVG zD(0E`2yK$B9#;xZ#|z#JVMckVETnH$Fv=~2WstOk$LuRay>oC^UGQbttL41!@5FZZ z@H1iCod#)x1A%D(hbyjM*3+p@Q{!$%Md?@VpIY+?k|GK-j(rwcgnb?iKegCu&y1Qy z=#A!ude%U`=C;Bm0l=u*F5x*raW`7H9ui;k0A;DL%b_M%Umu3-OwdqLg+{cibqm3+ z+xwQu0e;4ecNHmm`AG{Mmwt9F{8V@`ud`K@`5{$r6|)eMR$9G`aEi%Kdbpt*5cIl> zWwns>ub5GHd1eg+@9*HpuR_JL_+kgAx@D>A!$L-Xv)&6 z`;dQlY?H>5+^@@?QnfqsE0AY2Q_Yt!YD{iPzRp<;wQDE=)|+9hin#8<$OUM(`_8Jq zlaN&PU&CJq@TKM;EuxS}iE-gUps{W@$eYakJ}j)|m#6W%q3S85Fi6NFUDfhkOAgr( z0$*zLSkD4`mva5E`915zH=GNAcg1@2_+Df706ae|Lvxf63~XO^ zxB|2*{m+l#mc^y*l#16BbyMzUC_do88;W>-MNn`3=;crP`wn5j5JXS>9Fz!)NZ%SC z^0^w)T{~aY%fdP1XCnM+?Zw?@di(e)6|Z?N!~)tIY{g!UgZ!nM&f!NA3x>Qkr2dp= zb=G0m7LxjwIr8#BG6nw>`G~`$Yqj;8m!vL`mMo{bBeznwnwlG~A+1sx$Kk=P)xl@* z-jFuJ7>= zYp)3T*E%!l4~Km3KVPWVW7`^atlLYdC=}e4Z6bxB0TgLmy$6aG0@F(VddcA3D3O64 zzF@&1{H0{SpTBC7!(Ipz-hiRJge6G!>z+5b5StdYpW~np;b$oO0@}-!?g+iYjMHOG z3ute>&;=AVM?_0FWCoN-W9N}nKfUh`3mc~CNt|Z~lbySF?^pw{-L4lyD3SCjpD0u% z;Ovf&l1*oy*)S7Y!we5I2i~GKuCnFN@Li1cJG2uagAiQ7CmP{`@bB;{pQkjWu~Bfn z9A!;&ld2gJO;|d5xzJ0%e|I#xcv_P_jOGXb%M2RYxU}#D&jihQb}KQi?jtv~sqkLs zRl-k2aE$;JM}~s*x+41946|RmD!t+ zw!26y2!9zY>v9VnQc<*?NTI_2V!SH_q(w>wT(N}{cYz1PJ%8>DPOaC*t_6NwPv$L4 z`Ad?-9{#>%sT1W$V=v!Hl8@|I0C%@{e|x%l5t-3~U)EkN?lZ%MyM?BCL$0;n3dh$M zTOs!*Ug44;@8+hc8Y)d+17^1nLxDCfL_Rqu=4(F#wp%WVl^TvCO>zq_3=A5ATH+m7 zWImq32Vnu*opa`sV@X7&=dgMK7vDL&0cz7h~LUK1;HAaW3JC6%pF$%@JVptEKll;#R!3%sgxk0l6Bd=5gg+5R4@s z)U*7p=Ahv1Fn2NLcm5PK^9NBxHCzeI?~x8ZAe4h6k6*W55p(oRsg=QeQTERAE1?u1 zzT}J%DzzE_?F}w(ptkfU5nvm0)&Ev9PqI{~Q@^EQE&jmRWICjIA+JiMuHNk&R@N7R zn-0gl+T?_GYlF`#Z$Q>=b>&wm4aZ}~o1l5@xk>H*%E#9X3kPJN^fGf~vrY4==ncTP z^?HGFD(cW^+q^;P9j)dMqT`$h>9(8pPat;=p7=g?k7#eU1p9Icx+uVXW66nG-m&;x8XGR%3EdhE(g3}y93qFV8O*YSzT;f&6BlyDRG%lV9+ zrwN;v#0;MsfZon4mwM`_sw{KA4q~-jG1C=ISx=qfhR?3}Tb{V$4Hd-pY_bi9UCNnj zLiRVi`k-yx723v9^7Jet=Ed;xU9DzmdE%S|49%zO-G?tALd?!a5xnNFgk9M%A-P{4t^yW1spgTE@GaGDr*clT`0CDCJAHu&A| z3+<-f`{FgSwcc3qA!s`k2%4{gDW*@A>pl zAEi0I`ppTx3?pcMpQuD#aH{a?iw!bwNPBsistD-`K5zc@QE*p!#++f&5rRY^R&h@R z+4M9%2^t}&Xu_re`&}6*9e{le(bmK*KgHc}XQ)*2{3FW%j5NRv``s$e$p?gaej5|BDct@phgeb4>&QF*ps48AaJ zk11|&cj-7Wlo>io@)|Y^W9^pnM!$5Ch+alm#J~fu=-f6kk>~91g`BKi&p|Q-<6V^i zxkc~?$|W!I(1&VFYxQjA4PaNA{?j8+$=X+F@3$63bxYqDgJz2XQ?oj^&=@eekZ-H_ zb|}|3{Eh2ZbDqnqv%&a`)6jZ0ybey*@mUz^=CmlEw9Y%=CwR5&!uxh~uS!9A7F;J;n4 zcX{PGxH?ePn6zN_vtiN@yt-bg7J?MWdFCa0d)okM4W7PwM@vZ>sCz)l*unjrr!bXJp#uh-Di8sN9QVh?MgD75OageE2NFR+ENE z36j_?C&JYgpM}_IM$Yso`pzhew1rC|CAOr67pq;gy6_ag4!nj?1HDX3%(uR_@LR{r zX8pxcAaG@Q*YH~*5m@D7;ap({y24Y0&3B%n0fTDUQkQ#aJ_`+rNvkeM;D$xj|1AWo zH^}pz{2-yqE{PZ+_tpU1Z7b!6sgDmnee$;koZgW$wn&|P>Vvt@g)k3T>3CZpyMnwO z&u!;otloJ74*qyW!kBS?A8@;Qu!K+>deeZqT1EjhDPd9YnY6XT4NlBJ!;7liiPU-)S~XJ+^8H$W~R@oG5fnZ?j7Dy)p|{xAkDlleB} zhYiwXzaPP&ArhbJ+sF#CfF6D+3f&a5VfB$f+NZOqRpPJ4BFtQV-;ji<;v=^Z4h?}K zOK-Bjav;2TSN;WdvpnEx3UKp^htYwDYO^v&E3iBiQSoT)=U>3Qd6!&bSK&4kM5~{F z0pb*8>UN|vp|T+V$-SX#Pn+10=1;Pj4M!ektB zHS+0xvxk90p_B?@ZBsmY)Vl-JQ{YTcT0gQ(x5qWT__M?@0|)A?4hTsiG&L3pc+dD9%fzrZ!~g#8fOA4j>pc zsORUY&{6|(*nl+uF2bc@&ouOtpsV8yd?Q) zSJI7{6SprQyF57m-a0Gz*28@Puv-URi2hPTsZRk6Tk6-JQH{|4zI=0OXe}?3 zK7XNB5_^Qp0p|^@l>QV7@6vZN(a<@khsX?iyF4`&`%=hpug2fw9)VCi96z)`fkxhVm_k|3&HK|<10%0MD z4kJY-LCBD@CV(ph8n5K7{P%Xf)VDiS7X+YJz57mwlYF|EkNUJACZN2%0K@PlKrDs z=(n!RC=y9{nz-G=y+ME7TdqUx-&3)M-wI{*&eEIIuK>0fE(?SSJ`QzdN*F0?Q1Y$t zI2Nq9cUrp%B)T*#HLOF@{e1YtkkU**2=!1fNljX1R37PsKFy67_ zvxMO@cg|rK%7~2C{1k``-`z8h0M$cNnV{iXm=5%Q)9rtQ%zQCI@NH-__srK}m%dyS zE(vk$I(RBeCt(ngC+jgGZ!Pqn&zvq{tg5eT{FUn$S9)+U;coV-{uM^hTJQpTX{){# zH0&2PzEh42ts^|*V99Vn}A*S}YA419sPB_Iz@{^9ncIoj!N z$GV4T=^+$8_hS!EyyKnz4)cmo3V_HPNp|#(Z`IDKZvPb^UoF&vFLs`F<118H-6xd1 zs#{E#=kn)xH+ew0)GHPnLZ$j=VPsxE=X_^ClHxG5Lk$n3hBoWjU98}UtTjA(pt@pB zo+RNGlhQ6frywwSo0m7e#X}fzN}2fg+`FlcE(9nG^{!?4SIyAfRK;$_BML8uy8@nF zO&kBBQ5p^XwEx`U(;zkzhw;18;5($Sehrkc^``eVLXi;k{_cwzH1u}qqa=n3dA9x! z1k5W@!S6(Atx{Bp5;nhK;=OKsMRZA$^^ zg?qdvG_T^vkno@hZWG;eI%t_w^NHHjRJ5lC6#)$ZZ#OW>+dBb8YOnx4AW5zAxY%_z z)6(_>W=@Nb-bl~GWgxU%(o)$B9kk(b7Ac>V z9YA^7L&!M5bXwi-Jq)9T5bRA)mv8ZI0%ThJ$_ZiYykz)DucS@Z2#;X>n3=~psS(Mb ztPBqV?w7OVZ!)Z@$>;5A>X&qNn=g5H3{pw>p$gLS47=phx`5zFsP4jUr53R}(3Hzt zcCYPtn3l1jv~6Chu4X6^hlK!X_qv#{%`eTtzplV&+dgHqO@*-3HQhr}On_19N>nrj zl6`M>`&l$!8q$}FrW&<*&F~Y~%lY}0fW~H0<=o7fVYHCj?IXttdQd+@WW{GmHt!Sg zP;`@)jZX?g`dfwEw3f^Eb)KupuHS1J)ZD&yW;9)qZdEya_Z-5<`_ou%Lp>o@aPBuY z)L`=xl=5}*kYpjZ5`6!jSwgTHE+U;_grA87l9JjWh)M`%qMs%%Jcjwoie1mM9nu@B z@y?jQV3c6;J2r$19q%jE}-tQhU*BTBh>NYs*UY45Pm;*Ac|KLeV4 z=nQk^pG-XAU@V!9>HOAvCVo~T_O+BF`^=2?X83#n&@Q?dry27`9TMEa{=#nR+s_kP z6c6Du^3!DEbTXoA;6kX?t6MXXaMCh273m?>J}K<9KxdaX!+Ftl;Ij9)j~hIu-0WMe zaka<^lU@e(cJ<5|RXWM3)VvXD7KLbOw8uL_YFnrIP96&8#xE;ueva#SA&6{JxDbpv zIQLOz<&#M+TsVAy(~9-%`DDf{Tj|dnF4*BRJT5i3P=l1m9`EJAy(}5w0wSOp9yvS~ z_DXf|5}*Wgl;!(sZV=G#a@jN)GmaWJqPG8Cgx(T)*2Mh9F9GW|8XZ2VO9W+{x|h3^ zI`0wF%lFpi@k&-rg{<7?EeA1_}@Vk09UjGy7Vc}CM zWogDMVK>?RiaoQQs$m^uW}A+3p3%e~R5bQRA@V`tityy% zR2QtLg7hUSv3CoR+&L(ZQ56C%$Fz865OBwJ=+{xQRBwX&SqXwuPk}orz@gQo?xvZ+#EeZl<6F!!sKecKyaW{3~GW(He8nJ9qh7gM-Ax0Edv zjn9@pGJ7kwEF9^};-mQ-j#ce$tnR;r1q-pAhP1*vT)%|ytkL_>yd@D;9l3G=kN&`e7kcWo&Jp(3>nhQ8>ZY@D2=AD!6uEvp9rz@ zB@jXAnqOJ5e}>@P25VS)X(p;Q>!BHixk89IKUld-F%p_YSSz%g#!%IgK`PHs!fYUO ztsj#L2w!^ahG42tPx>jrxCqsB=wT~3wvoN_^!=x1aODmwQOER~4XLS5wg=>@Vz@Li zMTa_v>ucRSE>jDVMGEC+meM#EWp#AF&`_3~n}H}xWp#}0SHg+I9}Q0&k1#fAzSylu zM`N#;-UnMLNo=LC4(z6yCbqJ=maE^Oui~p&Dx7`fx_UXk_K9lQXlaN7tF35PK*xXX z6<)a<3d)z*#k^LlSI||2r0v|%xqxZG3dhsmI)2%$zARDrYqLw?w_?bg4>JU-#7;p$ zyINtWK(*$POWg!EUXoZ9!~J~l<)ZKutwe?X#Ql+Q{;BIS%BuDxMMvybn&dePU=-UQ zHA}AHVc5xZ6aQ875o|Twnhgifo@*s4+;F5+9{q>YRB&f;o0X;d$h4d_)gi_I;x0LC-_ zSQ!mkwiaKSR}gHaC3)iuO+6B2nmfX+fPQsye!k1taw#v-?-YvQN{nJ&>B`&^eR%kh zb|R(9al$}&8bkhXB|zM$|9{?2d0Q|;@y82@w#5fR(=#!^UTgNAj$GlYP|jR6HI`D+ znGb_jkBzTT^!zQT_EyhN?>Nqabl=%ua&r3MVXn3RY?_XA9<@%DK0bUq;MoS3!|zxa2UvFxWf@S9+T>Ho^UI_7V#69+kDt;USoM;Jy;x}IP|ZAQu-FQ$!= z6R7#pssh8+=X~OmAV{mm#|-m?UF#(eTSB1JuE7KM748WmkCOnkS2-+U{#fCjAg{nV zc;>U6NO!qwScG?k`EodueR4i*lPkOvltX`V_Tlfu($}07FmB&z_8KP0mTnv_2ayL| z!?A1<)j~nnY?7F21eOx_ih3{jd(%xQzb?QF;bX0V^Hh~=dlvAUOHaI)MztMToKiv1 zaD8p-isTZ{og3dUn_)@|3z7dT<#-}=uU~Fx_)^U`mvVD#^|#Z{u7uaKk6-F`YX3*e z?xt6~R~RXfXI}1iprL-b#NoDL_&l%iR*a%dNH2dSQU^zF@;SNt8!FoTK(GSf4OTMr zx;J?KBYgAlRp@10vO#S~uJ)d8C8(6XUS02^Tt{d-F03yP{^z*6SR$T}w$?329%IR~ zoYpt^mV$L$1<1Qs`T}6IsD{})yyc+YN?D)Db%gDu)0pr)CSd)Rb&egAQw}1gI?zh@ zeX#}WD6HdCM9NiZxtEj$yhaP6YME$Ub1$hnUa5wkb+F@@2a9vk_-^TLgRZ3-v|F=` z6%fO;T-_@ytZ1ovzR`$m+UZ0W({&y6O z`yYZu(q?h0$$Yqvin&xy%TDQ}8dO>(oDdUE?kxrNSp)6#*N%8!K#1CD3?h7KmB-n_ zQ%59PF!Q1(0LthAXoSBL*sk2^^eLfzD!j%*V~zdam<{|DaPI*`_7VFv{fxAr`vOrzhh zg~sQwUPIC6OCx6tyaU27)g6X?N4B2!Cfgtw?{QF#m`mdlNm(0uIiUysJxGAOBJX*V zF7>1@kPiDAVH;wXG)9cb>c=nCzDWJt6DKYyT|IN5Dymyno(*Afgi_cA$OVL4Iw}e0 zk2uUVtN~w}_k|xukhboCVQtu+?#!A@|2TOoD+P>h8{ATkiNUmt^ysJU)u_0C z3J#tgy=tZW)N?0IKu1LvU6z^=ayc_kB>q(`+sl2LLb}|?EiYQ?U3H`JNPa7bC@Pf8 z5mnrx{J!lM!kD}Cd=y&EUts#@(2Bt=$(`YD_|puFY`i!w5!1cm)eQJkgMR|&;jxC# zVOQ}BVMplm3txdnQq|+0HotNsV&YAbF4!-_3*F$3d+Ul=ok8`Sona;5T&6q%sP->b z=ASz7+*bKA+$xZ0varJFZ>6VM&gXGZ&6?iR^&26Po-_A-^usVp3hO}g-?@3={+3Ak zI=Ph~Sz35RU$|ev>HK!W z6C8i|W2*tirNq4(2_^*|>|q&22#P+KbA zQm!(WN1_imI410isE`y8A?+FczGTeYnn#)oT^V$z8h`Bgm-|dhq(POKcK`eU=*DQx z{+ziRSnX+T&BD-Xz0qY57e+0-GQ3~$HSgr#ZJ`-n-`OfkmU8A*%T|F(Cj>=nsVv(y z-=smGmqx+o>FEK@;M{hq3YxOU>qBYQEDf!vm=s)~w5+P(fT*BK&5=)sI~*u@d5%?s zeFCq{p0g-A2-}iya>5~Zofvf{-BYlK!^IFNGE)cjE@T9i3#=6`hCKRlOS5J~s`?0K zZZJ{E;c=Pp7lj9d`9k|zgT>5gj8x2oQNs?G?5}CctL8UtqC~U(9hxBM4*%HSR5c9w zYaov|z`JdXTO#fPnjFZs4e)Ls>sAtO3~+i4E8)rFm8tC+=VSbwxWg#C8)+!~1Vab` zJsik3ty^GG<{52c0p%9>R=Qa-8mE3FU3>8Iv~h2S^R978(6fY8?%`-cJesh0Gm1UP zQZ<;#G!S=NON}^KJ#rLoFC*L>t9|>$G!XgCV}>6JHbWXWhddfJ@T=idgsVas{o>@s zAjrwc{_j2Sh|{b65*S?<#u%Q}-v)?v<5@A2eo%^hddQ}kZ|;SEwDpkJ?a;rXpzuat z`~4Pd!M-ch+KTVS(K$WMv_1_FGZJ|JZpuLUOKFOU3er_~)j9l;)+n8MI>-f8H{v$K zvH|MvJoLHsJA}}XQnv<4(W%H@81r~;+`S!IgQbEZ!UqaAiH4)82hIvValdM`XS|)6 zQgUcp6xSu%EqB2*@;isAP6KZ@5GLw-n5Tr_a9?1o5HPSuJyL!ltFZ+EygLH9gP+u0 zz+hD4A89tI;}T8LFk_)bjy&JKv1sF>izYa61UKmhq?dWzGQFy4mBaoE_gsmmKH-Oef7*KW#r{g?fYispdDZFVGi#D@4xXFxU@(@l>cr4V~b84$K ztt>V5Y8jOyS0x? z*ILbt=zoNrS*|QQu0tnuf#be;^B>qtk;>dsIOGEA>*BZ~cQkG&YO&Y`payx@$6+66 z$;K+Km$w!dq*UUWFe#d&xxF4eyxDef9lcoKV8XQM?hPB%E;}#o5@jE2%QH<)yzRLi z-RPj((oJPQchYk6>apKGw{Lf}{ehdI5b-^MbC)Uf?#L1M?Y*d?nZu7q}Hqb-OemT<$7RhKZ6=D*;ltxZ+c%M z$_bQS9<3$Q`0op(eRtU#(@v$mp`31eA9`YwT4^yU4WB){>2@6ZT}Qa3NNn$ccf&GE zIx!3*1kXzyP_?*h+dDetUOC9V-y1ZsVvgzd;M$eP5Obu7Ddn&JC3y68Hao1Zze^SgL(cNiI>JTJ3o7! z+hAL;o6|Z?yd#G}B(h8>+OylkQqLL=+%70##U8s7X;)tEOrZiasE|0jR)^&K`nLSS zV-V13MsN!g%6`&O!sfH)ejn)z^##93xgj&G+I|U?dL8TWs8Wh}j;%p6Fx2Tk7 zAeU&`{SoNWNnl7bPSt|OQRowPKbAyGX*+-Zfr&i@LgsOAbV|#CK@D zrQd^~75Cu!v3j9(%6$vU8l+u^`xT^-R~K-^et=EmtH} zaQEApV`T4!f^&O$MOAcu4c)q;JM`WnmXp9smh`~sKZRD$lFL&KOX@CfZbqb=w~Z7x z1y!afw&t$LGNgmVJR4L+RY6pzRkb>0-@L<&7~(q%e3oveaFJ+7Yx1ImliXjo7z>&& za&KqXi>u>z2BLw`6+gP{2rnH%Z+1NM!;CG)H(8bJajbV59D2G0s%N#ryIo{w>wT(m zAyF7HC||h?vVl6+3(3B-TvlvpuT$l9y_k=u&cqcc%Wi$>C$jxa>QIT;`OP~s(RN_3 zlr-Ok38kjweku(yCEO1-01Sa8bbBXK8uA|R+Y!bUXP0}_De*cc*VBj5cnEZRCSr3( z2?Kc-1V*B~?cJo(i$d^gYgMHyzAP(!cgCYF_YCUuphHw+&9>96kNN}XgiTpu9!KZTD}+0AJx^jfI;K^IIc)s*dy0)uZ%4{@0pZ8KQl=X4`ciS+&Y&(5Ht zRi514$aRE(&&Te!>!;{^mSV;aH|b!HA-Sr!gV|J;)GgpwO~>1obvqPaP@kfK?5NmC zCUhfNB8V}7+=~|O4DBQZ%{TWJn*tF;3h}SH<25y30bsqj<8eO|{R;N;oosY`%i2gt zG?4TEwDD?vYS{R`#0D0YXR9N+bXs~nh$lT8@EKF?bjD7#x5w~n&Eg)u<+Ky3n|BE5 zD2S6mTxxT&3jMqzM&4&2Mc_K=Hk7i{&)WjN!~A`|-97e2cjm0OEa^(4tP3jWH1)cp z)>E7ma`r;+j-7=m`L;`r)CM;aO{d$9?0k-4)2Td*0*|5ND0G^7ORE=ZhG=>DN!ahi zHh6#AAsh}arOuJlm!`Z1C_p41QS^5^J)#@Zije*gR8@?1he^76U76^$cMBUfTkh%P z?SiIte2bzPV!3Z_6Gi2Bw|RpCC)AewTS{l|D)n;Qkx3tX0{1P_;F}zZvz*(gu6cKV zALHg*Q|2XOO_Q=~EAGZ{g{omjQ=UH(O-^nk&)KzJW^m0?@tBrQ=&r-&AK8r3yT>Lj zcY|oiOZ~M0M7%4EXa5^A2yk$nW_g@lp^!_uol*zo-!?)Kw?H*m9pF$LVcLX>TKr?%41#{|3T;?FfoQS2ib3vf-(h4Rsu>Ni z4nv$8IiJe(8LEckuH(xOT6`rP@x^AH(SZ&7t3@CT(i&C@2xmYwK<{1E{c%ito%w`6VD{Bk#!pr2fq0-^Vf&&MZ6qj{e9A^bPi@)#SiU{|#p#kz$ifEL|9y%< zEC)VRrXw~hxF1JjOet|YsHSd~qc>YtjnJN6rWhCqDzk7gvTz1}5Q zUb>di?r+U(gUDSZhrG=aT>~hxbz=IMRN7H=mEyg!bMt zLy8kRNHdDT7P2!)-|abc@F4Y?x5jP-2B{4z?~C6nF?+{V*+x}4CT=GrR0}Sw>h3F# zO0npkpjtHAlUb)^v?~5`w=ONkB{svuhF$%R`7)?zb|GypC67S<^Yg zZ9ggg7Q5HlCt4EBbVtS*6n}S^@GLiUa$a;?n9=E}N>OS@|C;x*TOJ7I^_Z;Z-nB8C zr}`ae*C?!cX;^QHVy^DqFus1olLq<)8M)-uLW4&pn>3FdQAeS8b#7=K~)2acwIn)9A2@usvc;i8x@@B&y(is*)2qYbW< zxi&U()2VG#bVtlBWi%;jjt;akXz6wA*{9_Z2@B|gv(wDp#~*oYsbz&P(6r2sY$i=T zDmRbA!eX|I8^{$&EC(;K+R9~Jwj*Lf(XFG$XAOXEA!lTFt@?{k4^Fq`+r?IGd2~|N z#ZD>c8Bp(1u~dsmQFpG;6(M{0&HmhETh?cyPctOvYi8wmnhGbGytAfDGg?iA-7a@i z$+ugf`Ay!@#bQpgtJ+N302%kuprnJkoa3ubq8y|v>Md8OQ)lS5!*f#L1t9k+d=}D* zTt~~y45iPH#)N{|T4u76+$)kv_>MD8_K(`-(wrwnU00qsIG%;vM9(CgnMk`;E^UCy z<8f2jB!Rc2h=)Kf9XzYk-F9}#O2zk7vDW{=IU~!T`;qKIN-^LRO=xIVb;&uCF5c|6 zoX@RG)kBn=c7GG+RBl7ubeeIL#Zz~ER+Dz#`I-fV_&oMm*`G-%R|=|cF}+{lp8rb8 z+%5Ie^<5ao;TEr1IGwDJ&7QucS|4LLi^kcEOIK{Jit};vbJ4Kzc0p2SCBit!z*K6( zh@ZT)tr0?McLlSoUnA}@v%|@UPSs)^rW2wr8vnyijm}(@VGpH~l5YGdAP8Eevg`(C zf_tOb2w6uq>XbAH!1~n;Hse<c;c`u**)3x*xjc^F>x2}pe&Qv zUFZ3Ho|*g9La?2|DB*fz+Z+rhtW2-#?Pb#?OVx32z9q^I{jb@_XrKwPufTETSGjCdjUF)QX+G{ivOCC z>KaleNW4+;M(SvEu-nxE`fchXBD0tDW#v`Zc{Dp2cB9384;oyVo$@%lB0hS!?FWks z#5dl(DOjv6(mI}aS%9ZNS-(*7}S02sl?nOHQt9v~O;$1o4 z6Nl;KDeh-(CakARN30wf8Nqmw@zaqObk?{M|?v z8s}S}3hX${MzD~p2{rfg;l>t6Zd-9@{O=_0`?@?FX!Nfch|VJpzw&2t9xgZ z!@FCQ+?X{L%#UKEO%5}?IUlt{LJIqKXPV_kBJ&CXjs+vCy(7-$9yz+hUx+S+?VA>k?3omV1>Di?*DeI;b z)j^fpC)ESnEIujiX1gbR3NKah+YYQc)xNtz#x%+z4OTOE7N)>kdX)XF^$L7V?)1%! zH5Yhy(86_&7R$H-uS`xDAyYA^N2E*2T%6DqHuQ^V&6n9P%KHc4t|7gGt% z(C~)XS#5KQX)pBvdF0xPP~4ZUcHJCpEM#t(l!mr@S2n?!J8U8YPEGEAcX}IX4Jo%v z-A~(8w>y_9tzm}D#>DQP^SP1NkX9VPjwnYFI9$Z?q-EFn&en5!($0zOHlmDLbF<8| z5lIvrY@Pfc$x`EmG~NJ{g~s9k$bC}OpXJTUf_rr}uSMVPbU2IWnL=958zS419jQ%h zJEmcF+Ges#cwR{z-?4Cn{t);wll|^jQdoR^azdY~RQyGl$$tYCR`%xaqDu>v2&>5~ zioAWTI9=-VaghF1-LE9~dzDK%Dz_JTCwIk3+D;A)+xtsk@3t%jp3DidA-hWMlt-xuiZxtg1P7NKoS&Q#IU+C|-hNe-fM>V^S8)fHV=F{0+Iqd+ zWS>ntIOMw{)S|oBrC=WC?pj%NTefVvDtE*^0yZVy*8EINT{QRZ%T`)arx5{e7u8j@ z1F~Ow#004T4uCz`EULnunSR(MGPdt7yl=tZn*qIh9%zc5xZOYRM_cIAO3+yi%P!J> zsjD}|oeJYi8imu*j3l<9#UnwWm)j}Lr+;%>)tKrP1CX@;@$<>KKa!Ta3uleZn#fIZ zA5wGtU{z(iUjNOM^>(4vJm_xGSv zyi_RpSohUZMsF^c-Cg5RvSeF^SzSYKMqE78mQ%Drn@dvv-6?LZ6NeuNO;%T3kbfbm z2W7YZWgk~Z30YKsXKU9S4e>WD+dRA|{`!!mbv}wt^roGqApy6P;%3f`SNnWnn8gOw z@$D;LcxM2vvC97>_E5N>{vs0U^r&2Yh7{1v%t zsnKgK?cQ7j9ba^*vv-Dw7&#z!R>CAe;BQug?u*#Cpo~(!)bc#shK{1|mLyWBayEaeL}|W<$YV5OxoK&0b{TJt*c4$f zCCPI9_ohJh1(*HbnRnViiL~1mdUxGq~2>xE(6?q)$=@v~* z{CCdo)V|k&XE`D*#5x23u^XtB>vs8GNyA%qsoli$dMwWopr9TzhZWeg^3GH)5O?Jf zHE$+0_AjKk`RTnqmyn^=#l%BSf zTkNlgStX6ouT0log6Y!T&`Af-Tkj@Y7w5``jGvTVO+# zS9I8;-=@qvXNv!=qPt%DD%$P!-LEf#Nbr-1R?Y0WDD$1 zu0&#fPA+zzwaMs33)|b)1-3*eJY~3H+`5!{(LP&Ml$XD&EGzAZ9EXlY9v>}}VrLB< z4&^WWRD=glcV`%xltYy^LsmrSj%yd%)OW!le)UC_!71Beptj-~oQeP0RwjE9jXj13>t{dVi9^=BRfjfRzN6k? z`ei7fz~37>9>z-;X@Yo-fY9!*Mr{0;v2K=$gUv28X;a*ljJ~&4)y`8Rvb4+?HVcHD z2l`-X2_6QqyNTxQtH-PM7{MdEy#Wc z1oG~kFDyg>pNcf$9spqt9`#tykVp}89RXo~#oVx6U&I2QG;?)VWJ^*B(SoRTu`F-R zCYv-~=P;}%$w2VY-F-mJV;goC(@$Wv9Qm7ncnT(aX&DB@7n^@qiQVe&eQT!`-$mw` zFWGwIW(eC@?Lvs+0_NdAyX6CO^R$VGU41@l4ZE@5v4uuk1dKDN%-UYJ3b{EXhV0LfcLFBVV|Xnd?Db$&Jas`pbK4AIK9R zt-83)O)ziE(XpM~pkgvub~C?e?L{H&_>t|`61{BQb$9JZ%JGVzI&xk%fp)8^8-7P@ zo=52|XZI^d6n%i4c5IEewV`e3xggoaZG%F9=-}?VAIVCg2}Pi6-69ah^U(*z-o?m1}4+6YhfQg-2Kq`55$p}S?x zxcgm-E(Nh}b}FHM+jA_bjvsKj`%BUtReH=5(u~XM%CdXX@15!e%a=$=D=wpbL&g0Z zEH0rv(liS5uA9Wj-c7B?8;`0Rk8M@5y_i>0E){*vP_o@mo9x|E)a z!~!*q!7*g(1beaMe91Qf#u}Ev&o^#nHR2iOjQl1E^KWHj&$I&v^G!o5hHMiekn~%i8`FMF1T++DIOt~qtl-8t^Z1f1|p^?3=x{7ioq$ zGoq8uN21(T`>u6HJjJV~4KNNivo&sQty|>XP{WDUboZ9jiL(U>npFiX)EekEp$u-t zeXN3@2?Ofy7S7Exb4jtmG;9BZM8|&zqFY0psm@+hUL0oFhF3Q-pUJe(cUy^=u%erh z)L2}w#AjqWqR?CU{ zj2s6op`Q8T@X0I#JgF(228Q-@8GlO_?t*$Sx?p7FFoub(x!cP2`_gWK^y5%<0#^K|4(_LA|h|X{z4*mTUj@rm9iUi zwePbccP~MJu+1Yv(2^K&>PLcI%syM+)Y0Yt%0~Blxk%%6Dk0y!-9-T~VSrD8vkLXX z5%RrjlD`_$@KM7w6Vg+^u2XnmtB7u@Z?r47TmNCd#>ZU0xUK zgvKnH1*@CuE4%P_{7X^5X;=&GqhQdQ>mh6>WeD$^TOyRfyKh|mOaOyz`mPD8ZgTzU zZwMcjnufinWbneP`BpJ1zell?{W2)(&N!2Epmhb_1vej3)LpAwjxQ29f^fF>XSw~1 zoZND)Xl;Sz7E89RQPo6U*SYu}H>ZD>!R?oz_0`m;jmAi2wtf;{d+QG5*l8{bpM=Nm{cwnWLZ2B-O=s$-gMVn;zqfu z%@uAQZ7&ndcu!I;dD_WB4ZAa<49J4b4m;P=hb*fj*!YE>49%$~H*)>_ZNVD4r5H*U zUbdev)m@1^d^?>r(oE1&@i#_O*UG%m>?*77&Aru~mb&_0b^q*qr~tT+ox1KQQ5do) zGD{ViW?z}kSH{^p+Dq9Lugtbfb!UKofiQ&!Ft)5@a_t|a&2N5Z=&3+AZ>-t!P{5-N|OrX_oVJP@^2b4zQA;g(h{@#kVtEgGSZ8Hz)vW|G zm#Vv?gLc^HBoBZdw=j=;V-rC8OTQ3e=$$%37;lT^-h{mF#E?bzD`}J}aYo5T(F%{y z_!MiRYd6O#RsDt}I6UrWAWzn1#~t{Yrq9m@W&cXLb|tLyEwoZjE%=r0DG=1);t>uF zr><{}$L>KX`Hs-uK^r?Z6{)#_{dbTE=K01(Sexw*UKWm#xA4%$o!T}EGlc4AK*o_i z*{Q+y;(jPkO8dq+#c>cjHTx-%*dSKNHxq=IwLvg(6R#$$wQ}<~7M_!`Q}C$xjurA? z3W7Z7t#f}NnGI`=3k0l?SZwRfP-1XY%CS0~zl2Dq88T;WtsYnTq*F?c*VU~S^zn`o zLkaS?5k61*-fSMPR~QqIR$n+pYy@FV(0pH-5GtMTrB-)G=*7G=sg>{J%hL%}nyfZr z=LII;#`B}OW={0g)sS3Luq~t~#kgQ|C13~Z_Sa@5HCUHpDQ7ktbBgV} zos+&ZR{?2i(wb?M`ixZ3cU=09kh+d8lx;_{weix_DxE}nX7az5Mae7IiJ^BkyReF( zJDk@t=Qa0!W~#m0CW{}|4W1*BIA8|S2J@uPFrOF<5-Sd80(fu1za0fgly~g7Oa@SY z_#)7Kiq9=s5i~1>cZR)G@YG?W4iBi(kKOTgSL-RM znIvAHNAgsMRh4oZto$|WwhJ9T5lmOAtq0`H({v8atOY0sYzLQn)C4k=nlDFAEwoq) z_GjScCgjZ)Nv9hCrmk+03r*gaWMLE_AY;#NFGss>Dl7Jieoi_pm?e8t31n!iyn3Es zgq84l`??UwD+Z=$=Lu8^tZo)g1x7rQ&!a~#vKzPZ$X7B0nW}aPLCtdd1H#ER6{BYl z9EV`fgDe#}y<3`H&5T&vrN`M-va7=uvi)^a)vOw?lZy7)Qb1Ree}YPIeoH;tNb3Z| z(#rIi3U6Ke)(gU@?P^VApOKj=_S-=^gNP$JHulh-q3$NB1HLsV(Xqqxl>LBc*e&in z{EQ-Plv;DK(*zB>eMijfIaqB_VI>5g45zF(=&s}4NqFY^X?a^MH)Ax_G*HrpKsuz4lqo9Ep6El+i zMzVQ$YZYsi)WTfsGoYmFisDZxDyemIEO)iar8Qj|@04gUv;FX>?e@?eRN#3hki>0N zAnqn#&v~A1U>X1?!oHytchm0It$0Q6vb#C-PVHE@p=VUg?{4o1X*c^k%jeC}_=lXE z8%b$bf^XlXkk1L6&FAh}kjLQsw zh~JTS)*W+bVgxvew0EnQXJQ#Vak`!yXGRJvyGV2ezPx30!OoH?VA}5x;x1ICsDEKV zSnS^8S2LLmpi#Z3n1Uk0J<0A*W4BRl@f0eYQyh$nyBJ;;**IawFL+ zJ$BHfn^lk>t>Lt=tN_d|;I^4treYf~c$P2m<^Y(IA z!TX$=h(ClyPLdw*2ON~Pv>JLFQ+=-OBt~6VpVpNCSaU5?LW!*mJ#XKWh){0Iz zyQ~nz8|MS4q9hN`Er65V0jpker3#5vnY06q(7ouKQs!4VO)&0@}_e&<2*l|f}UhJ_C ztCJbw(_~jA_w3dWV*hMSYtYBV{`~ifEW$7^^sWwElD#3{ zfVG$jPwdft$klnP9W&g}3qXa0IU8LCN(uHJQAABa?<#gXPF!HTn)7J97)NpJv&k<2 zsLK>s_eHmC8+!>?2hWUGD|d_Xz?N=Q_fuJG>>(E(ep4LzxBJ#9>lSOsJvt1O`1!c0 zj8~gi==RXU(fT8P)`!=T^9*()Hy0@2g?@m3W*GP?90}!DfG``%xj^R>jBy? zVlFDpHVp1Fl6I-qUgZ$MP{BxK7o?I-r&K@L5sj~m8@PXS=*vRg*&I^c?z3cSFFQ1& zFy#)dkoE{r$lWa|*74C;gws*7)%QXhFHUi{91tiK!%l4_v$?*;#}0(ti`G568%R4t zi~&HqJB6R&NA_QObfAPGn*#!IYVOZuSgdWSET=I{{>r}H6PZ75%yVX%s7}I`Vg5a9 z(|unUfGR3x3qb#jWZ@{b4_1(qi)!D0GYqY~HEgv@NS_9s%f>p?LVdT**pSNI2BZ=G-1f>EjYaG>lnl%9p!W#5r6TsJ%r3&??9wPnvng>2oMHMyy! zpbqy-88%^S%HqWP& znE3uZk9s7(mX_Qfw2nBq+)t<^Y9h&Bi9VP0w$S0_&->BkTZ^i>d&CP}rX~FI$J^&c zG1n*L^|SkuhXpHI7gcm^lGG7bEq7lQ%AD-`-0H6_F4*A|l~=Rdtx(ZzEv&&b^wFXY zYKzu|if+!aO5VO96TWm%zI@&CN@)>aRC=^6l*SYrx!CbOLdHjOf@`WNO`|3H<;YeQ zTw0cUe)}ZLx3fWp%8q?}T~nviC9FZdAB4`iyV*4+AYCGBMUw#J(JHG2Vs3R-mBRv1 zwS3@u>Iy*&GnY$WIlyn^C#vHTTUxVWRMB!^@{WBb;!f*ISJSm zuS9?ZDr|JIca$#O*+$$dm?)Iy?e^ic>UN>ZzX%}d*Kg*&35Kh!S}*qmyaXvz*qh00 zHGtoOTT=G7E!_P|%DMT~R|*ylDlpA@?+V?zefBs;xH74(xzhsCx;;kE7Vb6CsxsZk z_3M*3z6FRh5b?KrqX$!_M}Xj#aVjPGany2Mz}?Ymq~4QaF({H;j4xq@?kzd((f5^I zd8$kWjonf<3aLl9{br=b+P4@W!uBSdf6TbEMQxv-Epk+cdlUpT?7U)zlz=PnJi5C( z3|EV-te8QuO>*iCKvYovJGH%Hs;ffX@94*|B zqm1i7lux|4{11#rw=QU&TFN&rQ{gGdS*!~kx^w4C&An+rY<0&V3c2|bbA*hHiX14r zf1#S&!nZ}HCB}W`VJm5cimr5Jz4hD*q)ZI6ZUaw?Zq6a`ZR3MS(ZJmnRux-~HaScl441WQk=`&@U!wnSO zoNRY}KC7-ExHR|Z)ty_%MG=FfQDzi0tuLUEvMUW_(dzQsXSoG; z(@Pio31oBLzj4xk11l-JWrauSl)p?^7rQAv+P}8&)0;@dxW5ux(`+x)b9yNn3|j3@ zuqu$@5;KR+bx}YBrO0vTfS^nfXkGLH(#UHMX7+7-7e?#f^vn3Pvcd%sJPsxsYoC8E zbAHo5Sev^d0|P?Chgg3r>jIK;Y5qp?rQ#OM49^WFo9uon-HT{W8mg8FN&zVY2<*oXuDd8W^m++gW*zXjxEWp+Eje|ykQKp-Wj(#b?qk!5wgJW zm-MXHRKgiVqzUn*nNq^Cj;%@`Z_53~bZ_+CRI5!_8hKNW^X4ibk7B&CO=wkv)pjJ0 z!NNu+-Lh8Ds+5La!i=m!Bk#0ZQW6?w8ZnPf?keVQ_uZ@b+iQ&UsA`nl+5Jx2ai5?5 zx4wx&D{jM0DzDf&UpvogrAuqtbys!Y9(SbznVm?-w~t`z_BY+TQW1CUPRyh;)VAYr zLNzx#!MILU@XSgHHwFg-)+LR6*^kYfh26Snu5+$|O~v=g~g>*9*EdYB!nc0L=s zSnJ}el5Szcb+V*X+OWEjq%^PDl2WaUpDRDAbJm#$sCm&3ivFQaJJ>a=@0+>$ZpVJ8*RN*LC`4W!N0ZA#amJsyoV%NrX2WNiXE>U zm0mx&_~v>$L{LW`c7|zW@j6^VNvCNTWmaiy%YP$@ zWtin~3dw~UGIpxHQvFRyLBSK6G-&N^Zf{9Z-z}V|(OA&QKPz@5iv;shxD@amrl#R; zi>BGvixeyVWbut!>7XUe6bQ=_6Akx_S9Sv#Io-;wEJ3ltV+0$i3~NRdeEF1ghOz=~ zQ#`{C!#Y`X!`|GPV;h~kkjGw1QjHjvZ3T|TOtod1C#w0oCw&{R5~865rQ-(!m_Y1fgTP)YKhpVJ`p7N;?kXX}#K0y}yw+(@=J z_87*=geGAcTiveF%A4)`m7gbv%!W#&GMSOgxY_Lz87_Y&BG}!VLQI3`RsYU1A~F}J zJ^D3@yelr{XPUjKyV-$_*sM5tPbxIb8PC~`q(Sq7cAMyy5k9+H$4K987F_tsOatcI zJqS|1cL?c+1(kr|=02dxK!F_Z3F{ZQmBj`3w=p2BVV0=CBTEF!d(|168r9xB+a+;M zz0o!(?K3d{^4niT{)sXM+92EuK&98G=0210vepD^bJwfC9-SDxO&t{cvA=uMxG1f3 zQ8z?E#n*2?Mb&z1zeN02rQ13NP-D!R`?1eYQPRYq61$m=DBkwoCy8`W&5UyIP2)Z$ zg+YZ}KH-S-vkh4d^DQcp{eW#h`3{X3UgrWiS_X!)`mOo35!EnVsCs(eSIBPb?+JFE zm4bp2ySfNcwqHIe4QYk1cK(uskc?&qHnz}*Up^yC7?9((uoQZ03M(LhSU}mb$s<_Y zWqzyc5~(iY)@MZ;K` z%@PN*E#Qo{d7x3|En{7#>rFpSwIY3+Y)KmZQbGnx@)5Jq=uMI={4Al16XiX|~!~qm%CFwS~pJ_dwVhX*b)4C+u%0zuwC zn*0qF%GEjvI|bf$nMq(~;LhLrCp!5IKBn}rqBW%k`H`JM!+x2AGE#+L(X-Z|o_Lr} zF|~vh*!0Zd=t)^*Z){VkowJV#ZKh_>;spC*n@AMBBnjQFa-E8Unv%f6p1D)`K?LG@#HTembqYy}Tfy3IeB4wOLjY z?^JN}ie1eUi9ARWv2R6IK;4SMk;FrQ!-V^bv{Nlz$_&`r!$a#vYBW{|RLIaEZDa+){ij8&bYZfiTX{Z_VmSO;IPR9bSVvlfuB;Qn-W+FtdzoWrx>{Nf5rax+I& zA4BX=1*sXpnC`wv=Bm{mHE*!#8w@jtB<|F9CGYTDiP9HqTkLMJL)8WSEVc{F*C=OM zJC{<_t@ge=k0fPl&s1|4Tt~CcfjOkbt+eW%Az2MGgjpq6R7JOTYhR-abH?^6cs$Uh zHSF2BD}A?J{Ef)Jc{ma}L|s%Ydcw~tdqN& zoPnjBO%P}iu*aZIK^NrD!zSE-IfOl@xG_8d$zy-jgF5@rV3RkaSYS+HKr>EiiPS9j zx9PhwW6M+hF7#Ge>oo1I5U0c443?yACFic(-FeHDZ4DdV=6@<1s@FS}y{b8}y4jQ5 zT}Qz^IC+}!>Y5PxAhT|Tjm`2_v+m z9_I}&eX&fma{K^0!3!=^v@=qz?64nGmQ8H3b8<%zD!G8`zY2Qncfnt1S3t_RPQW@s z5rrzvhi3{o$LUL&E!L3NmlmUAcU<~HUGlKXs=yoG7C*|H`1cOt*Oj{4;={^@ z4^JYQZGE@XwmW;jx^Gd_=*>O2{ZohDlDIU!y}D0stJFVVi8ETBEiUb}Z*2n@F%v*5 z-VXBBsoXm2)_5ACHkUp3jWR!NjdaPjP6Q!!w>HzM3uN6(^k`O~bMa%8`!X+R?Jwr8 z*{VsWcsoWMp@6BjAdAp_sS4b6c?Jg~rBQpg){0IE7bSuKhm=_KEX#f3enX2~Q{$kbyIb7&Ku{CI8%Mq;bcEU#O$ zM($vi`bqv9(|#Q%FJsbr=@c7D?FwOE8hag>BAVP?8QdksRt`Au>PDEM z-ug=#=t!Cc8|%jigM}xtx$9|l=)Fg!(S-AOa5U)-mmGCacnZl5?)8*z#AxG>t}^sY zT9?_Nq?^NLL=rF3c!pt*To0<{fxN9Wk)s4?Mz$l0?uc4RjIYTKl{cvIrWCvI78>T| ztYJ%#gOuIT+vP=5g#ew(=8_8bNTI5Pg!oK!!LTD4VAER+L6dzh;)9!e{OmNE^_BM8 z36q2A>Te8LyL-m>6g7ZC@~wSk2-}?(I%)dGhUIMSy`yk`sj)@B$qSyV_9B~+^mnHm zHEux#Db`FrsPB%_FN|5ar8tPWTZ46}w2;gw#)HJd%5akm_1$3)jq?ThRiE|JLD#Np z)Xnt;woHtq(NG2>)mDzvD^c9V9%lKcv426iFP*v~eS_QUX0^(0fD{@G<Ew)xf(b{Il6m}B75;75k zPSfmg3sWP*&c$k_bE!sCjpXV6eSD39N9$SM>>!3@01NGBPWEn%gWnpgCcx;pWtxhv zW1>9Gda_wQ9IOMfZtg8KO#+BQ7{hjZ3ep(RWczwAlk%E#c6Wrkm$WZ*;qslXt#C3R z>!#tXDE9uAnE3@Z>XpGOGLBF3SkUo&x`$$ehD-# znt`l0-jD;byAzJ2^ zJq)rB^SH#s$X*SUKl_SzM%ttp@<;v)Du?#B4vm_Kdux`cA*gvce*K`v+rto*444DD z6dfpR4E7uKu#fUDZlDkMTPrIb`$AJz3v6+*m}6)_dAsS+la?x1As<7}Oyy=DAaAG) ze}4v7`>@P8V0ECk*jFLdhAgtBRyZrkVn(F(=dZA@7em#Wg@!!i zCQ4PRu`S$>#r=fBKMyH^m z?Fs1~H0Gw5KgQ5n$w0A>ZfiJJ*ytszUB}|%Rs$-$fTz3{i|}ecsThOuu2&e+lc_)m zk?-Fad!4e@^LwpSUy4m8AA~WaIQ|Q;Zkipd{Tn9~-i)%}``_4jV~R_un}qKo=ju&r zUrH*l`?(2aH!o{Bucz@}I^eoBj!*J;Q;sYXBAR9mqUmlR$zx+(%<+wOlIoC2Q_Z?F zrf^`NEHkb<5l_+>fbVzTa`b>*KlMq?m-9AG-#Bcvf)BaCjGm!uQ>BfgTPjDe_>-?EezB~hY;Ru zXJMb^tU5l89a)2wU9o-^YB1}?oX!#_`oi4w`Gqmm3k0AM9#-w1Zj#1u$vM{C4>2fO zgXt^`vRV6*w58!!F6VeEb=MiC)(K;P%HYnlvvQ6Z<05+&;+9#7z4v8*ZL0|nl%?mQ=ZTB8h#cict zAdQVVc&=G((xK}K4^QV^-roQ{J5Y(25w1vdx%tJGy1$IHg)ossb~*YKobzWk4S(Hn z&SlQub{F?kSzD|vKE{4|T+^OWCZP-K(jJKhb2JZ1f5qQBt$MC=J{)PvU2|95B^i)a zE|%0nJC(PjP9~~>0(IsYg1(>JQb3PxvUR>ZUsP@)j_ry7F*o^_`f&5Brnaq^J0a%c z|KPQ|PlJxCQ)6e>PUw2+#;6plM|Hn*{eoJe-s?t0eO_*_)72SOFO^Ij^q-!#$t<(;?m@a7O=k;hHTwEQ;hMh4BB+f*OM~tEb{{QsfFoUen&?g7s&Cm>ybNS>tLAl z=(=8`11%_y7o=)g8_*mqDmv}sxgks15u4lxNcDEsNEnp@PQrue&HYxP>K;2BW~L|f zkGiXHr_#cYRX+2i+Kk)V4sxSh=;BeqTx@KyX2dyJDe7+vGUh)h_Qh%~laj6LD?SL} z4Lj-OPNS1X%Nd4vK}m)0t6SUZq;xC2d+*LY$Q~IUcl+H@xlOgjtEK1{(nGV{@B9OH z2lg~MlOho$yX$FpQ91;&#+k3WW{*jHAHN-!H#?c0wyo=9ot!7M^%eKWaW<*@a#vdVrcBuvh3c)mtZzd7`1Y>lL4-WZbB;_kw=z=qrN<;K6}_CKu$eYpZe?%e zx<4IwlV;sMQXIY5zToI$bi_p|ch9($bmPZXIVqaIawXcLTC~0-7%jwc>wKGwf-bgM zr#@G{a9tl{zM$r&y;q##l<41?b`o9?bLqZ|Y;BtRR;HcW7RkCUK(Rb~bIa7tL3JaE z=ZKpJ5>Qwo&2vrm4$J-4ee z=Ci&vtk*BPIlHu|MFxW_#?;eKL^QgO;yDQwugjBM^O2y}OX~jJS zR*m|;a9~B1U(b%oMRB*3&=oH8{GvzSHLWbA>&9tAvV1P&O8AeWF;=2PJqVf@g+iSB z4jHAE%Dei8uvi%meLzFZt|4uosqgQ-QOpzuZK3WVd(*G z*xwC07~GQkGze;wOsTdAx2W;<71y^=6+koaw2tuA6+5=|ac>tvy34_Yp6T6* zrw2dD50;}-yr*ljyc6*6PJmwu!;146JF^n}afLKp5%B(!FrXSludlxtpQm!w<&y0t zRuCAn?IT$#*M+$@jF}}c&feEWOh?)80a!^5sn$TZn3y)=zDE||@Gxd_w{)I4UC*f4 z*0c%SouVo9jvmI26uA8uJLnW5T6KG_7XZCVlG%YDmqw?pm^BAm2ZGG3>2Hz7&{o`L zJz&@c-qZHvug!KNXKH+&ZZ+A~c|_M%uS?BHH0x>DWHLa{ ze3{5)IR*6IuD_d%T?FeKY-5mG$t$Sc{%HhwjB>Nx^4+f4e!`oEU1`!k5AO(~YqTvb zJ7)YBBS};Hq7$nfGTA;bE^S_jE`Y2pn4G%3muqlQQO2axMrGT?v~|{y#)5| z5X;c&zW8*ylG8umw;kDk@!3&^4_f7)JIhUtH;p(`2P-fN*N(BZiDdv<__ZDnh!o1m z*`{4=dUyR^FFggSp^#Ab+2j+3y9`n_Lv-fSl6 zFzDI(#U^_<*`Alfq^dJ_u~Pk}*xPm}Pl*Le(c#-3EOue}deu00UAhD-mTq@v@Z#(~ z!d|EKG@|XIeMjxmu&eXNuqUitNFKB4KhMOyNgQJPZmpNN&C0f(GOV7xY2ID2_fd@EiN?U4-N?!m zihZ%HPVQ;9t($CGc+-yLzLLJ^z-;}U8-j0fO}C|;ahotG+sfCj8YofYSV3{gV9~ZO z02SYoZ&*I5njy$(x{*vE$uIA>+|NahM_cZ;oRIH_GK0)NW;j_9ZZk@)jTukFvMbHc zfcV``TJzE6FRE>oYK{GV31}E`04=o8iVhKL>D`oii|evFob(H7-n2WtP5kb@z#Zlv z(k#0_vNH?lW#?6xu#%r}fXx^+VK%=N=oa$KCH2%l2A-Z$c*p~)gf*q*2A*qEJ}|~S zJ=gAjCQjXTg)0Ama1K|Sn}0j$-lJTy2@&pzy}G?5qtsFy8ZP}B+yOh&!tUij&h%$1 z3tP}!%F3pvH|2JLMhO}Tga`jqdFW347OcRygIe8Yfg-T)dTTGxFPSltRRK3e;F&`N zX#Lh*+!l=fUT0W2vFU8=%fmK`>`tCtA909KqJ_Xy`!)@}HR?%Ej7??BvHht--43y( zD?#Q)_>ldAmBc&4uCo2x*vKUKcch^V+-g{Im8Zdx!)i;L?%t8Zb0rZ#TFA67z|by} zSCclb28wHTODO>jZq;9>9RT}8hbr21_`3CJo#}*VKX7i?O+JHb4|{C+&|oVSpXp8+ zr11K(%{&c#0IsCwek+brBhJ<;r~=>+maKNRY4_bZMzsJ+070``)n${#TlNcWA35&y zNJ(nu-%F#0-*!6=2@@3L6WiXo3F9sGVWlk~1VVn^?p{viAMT*Vl6=mRw;R-dtu2u+ zztd1D&0uyQjlQ$QnOYipR9x`eo-ALxjox$0Z<=i9$_IPWP?c+xZZ91KvW806ni)oI zW}Jl$k#p$du>YQjhD$R%_vzSe6*sdGzw5&0>)G2vUz=O87xM#hyPymk8?l?eBJ%@w z2!)WGElFN3yQztSeQK{it7c^PDI2vOTDWKeG*9kuA#C?10&r+xLJSd(X5EJA-a=?(`cvz zd-HZa+p)M#n7*8r)bSez>!2q`W7EuZ)e`Xy)g8I>S$>k`Y@3JKQes9@i$1H2I;UK5 z7}Mux92pLmBm>OESkY`p)@LN!^5rEsz7Cuaq|!Bw*P^AQypOOfM}i7GId)}hiGW_e z@ILp4qVIO^zio*Ohf-INB9fY}zGnaX8O4Uk5n?6^X4J~5*xeQFFevfMCbdqO3+x+{ z&wWgL0hx;X+CDx;u4T6w%_rvo&d6dP1r=dw=Q{nHB(Eb|Ww6e_Wa-S(zr z`XA=o##b};1miUSuof+4y2IDHwD5~(VrcGxx!NGTFOu{M0=N$3N=sRf+ATbwWqf=K z-E`HCT&x(qBh4^NWM-YXhew{!s;?Tt*VmEl>r9U?j1X`7Khy2=<7ECBMk>sXmq3_- z8f=aBUKTdZwsZlBRt0!l<6e_x6og6FZ1NA_4@nm6Cp$tCSPGR);O4C!^51yE5^f?0Bx2sT) zsW&u?U{X<^sP+x3T4)MOl6+%iXSm(uPLuEM=@M|lK#rc>1+n*a8Fr`FBQslIxq&qK z=6y*-Z)${;($L>TQQJul8&3z*#0q9SYb_DT+e-M(NZ0!~L9L&W<43W4V=U3cY;)~f zV&2+HDHc;J$y3cghsBtTxHTBlXMr)ixRLbE%-A@HRz>8Mv}u1UK@Ps;{`};L+Zow@ z@zj0CrHozSC>KWOvXcySU!NcSOd3iWS#1G0Es@QOc-mLBX@6@s?!5FF*M89GV3##< z#eqcbVqP}NzS8sb3S(wil*=G(i981ogwI@MG<8=u3pVO6HLMU7ID2J^k+2avlGe=? z1-~FfurY+r-4AxQh4g@=&A0Z0Rk3%C3m3UDG9g>TT1zB2`1sIuk{#hFPHwYGwbz9Y zmggCu^3++YdpA4FN@c|=TGh(AH8Fn%vb!zM%h`&>$WpgmD4_Nh7e;YZG5J!v7p#x} zu32lOWG?%?Qs%9BMr@_N*i++H_OAL%9DaZdPoNlUJva61Jv#C8Wa z;&(ko`H=avV=uXL1_8XtH9D<^6Df!2ZP@mnLe$N5^q7R+6;L*c{dy(=Wvg8B4ib+; zE>-F?5^u{I>k@`c0lFl20-LhWeN!+c%9sf*?tn;!1IRZ4`XRA%HOoS5{xs6zQy9f4 zn$dE;X<&ND+6szi$QiqoHVk>1*20fDnh>$0apF zkM_l%?HR1*V(N}cOso9P=dd?LUA}q5y+NA7lHG=b6bCWJzQIL0383!ewRmQZDcQiE z0bA4D`Du~$;qdJFw2`O!pih2Aa*xt3X`f=e!M3Q_g@hL0OBbR}>||=WOLBXgZyBhG zeYw@bu3A!6_dO9@3LOn{P_AwZv9#YLN+J{eQNwVOn{(;zb)6UoEK^w{HMu#7B!@ox z1rlAzX6nP<8`X7dXkwNM73~nU*w=e&KJ1F~|C-WevLRWwL>h02?S-N)uw7!ho$4kG zeuT}42~8b9j}^C@*lLD}2s7iC+u7W4dXs3^Lp@kZ7uqNNxFVONal@=j=NuZkt6e0v zCQJqM|;`W;AzRArzdMdqMkbYtdWX~`ln!6vXy-RPs zilSyx7}?4F&OwC7h>I(Y7XNI0tSwXRt&aP2L|Jna<`(^2NEu2Yg9WDph(*(iZQu1_|ulG5nHu9pqQ6r!N^H8{ zZb5^}3yWwZS}=^Xc;s)(B=^=`;us9UxD1fj?AlA1gEt$UetFif-2Q@+uZt|+CwC2d zwPcT%ndIKxZg!K*z?E!0(=E6CJKhKQkp4Z;*5rO{r;6uLtj{MYBY|g__Wsc3JKrn= zvhhkP%(x93HDAZUJK(65v6*>I_dA)~7vqr8$cH|eJnU>r%{QkoJu&~-FZ zhJu!e`b`r8U)wK-2}m>StMi*?O1^H@Jum@DxgqxN z8j6aEzXi6#jtBPtGxBBcdvJ6I)U{!fSzD&~E7IA^eiGp>Xu+P<-A4B9?>I-Rk_d@v zlNm?8gVcrI(-yPk%(qOmLre1lnNp3+Rws%}y)fkE9tzo6LOd~JYIZv69_rkJD zKmT0Oh%w0z{XRfK_^sL0YGaD+AqrKr{mU(r>riKGmy_^w6-V4I5|VHBX%x>&b7L;~ zXSKDHA4T5_1QZd)+(+8n!_UaUCd#OZt|0UV61uA)W;%LH?Uy$?Pd!_0`^75fA;;DT zEPr%&x%YE2$Yi98mLrY>sB*M>3sy^$YM$L__2&+Q&}q7ttVZ z;yv0}J6Te8luj)BJ#3?DOImS%7#ps0y(NNVPPWb?l9$Gp)C-EL3~ecO*M#f~)AA+X zLsLMTlhGjiA}%4MD7B^Xl-svZ_Fe1J#Vi{TKposMi6Lyo5&fR>{ZOu#JO6L3ES)U| zg}vmT{g?aq{S4QcC$Izzc=tZj#5*o&F!M`wFU!q@kbARtGCf;ZrFn`Gl5XVU^3ZMz zOQoi5*NMGZXiv(yIp!w zr3l^dW49g2ZtL0OAPz7Eq#$DD!t!VBb#@z~? zyaHT$hiNrp4wp5f70SKo5`Wphls#d@+L0O>JEJ6SZh}T~mm}-B?HH_7^*w8LnJC2e z))K=9vdo9rySjZvQL(qw3kbPtxMa@m$FeLr^J}!0@W^F%Z71JComc1wL8q?EjXZ|$?{!XgdtAurZ8d6i&B$znKfyVM z3mv?kZ&FY64&gGz{Ysi)#*FJ^g~T;=P^v=Ry49q_eWw{nN~0_?R+np z3SmA33 z@1$vrku~X+R(_)S8yQu0gjg-zNpsxK=yUknCKJIZ7 zcURsV=|2KOt+s$uzIfhz>2@i!>gVD1aK!WWc`M{Jg3h+xP?63nF(;8xHNo!aM6c4B zfFl$1K|RUeWLp=()(RBdG)C-KtdKusj7=0a<3Bg?e{uiMJ8b^EcF|V+L|Qk=?*s;21+gKd-U7SfvK_*u#m$LKo3z-nV-k zx_9Nj1^rZo2>j2^%apInApOP4oX?$~rq3)}t|{Yxx?PY$3Ag$AE8hwUy=kS`iQedgF4GSr8-C4T_F8J*v<8r}xRFiaMiw6r3bTIkS%kgB zvJ-+lX~`J2m7vhSyW>g-K>+eKZ1<&;$sp(Te{W#C{3~;5Rr&MIno~&M<;iUQC`s%5>*J>b5i0n-_zKA4fO;jBKR1zT8pNVJU|t zcUamQo;5U>kh1cNq5ChVsz=c*3~1OhJkW<`sc9timg{hfRt1x z-eiTdvw?OeT;gI5jdLhEd@rwX9XmRug2( z%dSL2On?qBV;J`t6P4HrgBEccCEIFhzo|8B;FU)4iB--HBx)hW-a|GXY-#mze;-+6 z4jcD$1`&*ASC5)>e-c~Ag|^Lv#O|IQUXgn1g6eDuD{cj;X`g{YN1M4?2wl~x(v<6r z^#}Dv0uuWJH24{r!)70O5I~Yzv8OUqt){c8jdxj!71{oCnw?6R2GfA8M+3%mf0BX> zV_E_baq@}EY?}WDs*)d{K7dZlgwNXtM-%QP=OBYV&J9>~KUSepIfy4rnypjZ<#^hW zoqZ5H^*MeJgJ$cfm3e9|0iqnT*--bOl^JSb7uxQx&48Ju{_J|7tID)t3{-}`i^A=& ze;qBju1)AE(iY-d{A@(Zu9S5CJ$2H~qr3mwie{6Cwh6H$Zv`dTBXy-V+ai@sb{L9@ zY5p(cYZCYDN`vvn#%4BmLG~2Q5hDl^J(wj~Gi)Vcx7#Mb_L9Pe#@68;IDy_H3~omo zn!;P`x>}i~=Fafz`MwYrQ(Tuur8JvIT$qMVM^~hM(Fj@?BaDIue=8ApYu^~UaNUaX zOyX+nNccQRJNJ>Z0J`?^Ie-WNlT)?NzWzZ{qfoTU*4Qinh`(8h;;ipQc$NueZpS&< zZ3Q$>t@Im3i}v|qL$X_~I>E$^6ZohaMFcyL3sDGKOzX#gabFZYxQ^=iSTX^h#eU4D z`+P>`9|-uh89^J^n)NSL%Cj!P^zg3;%}Q3i{1c z(~g;;b#UrzwmY(RE%@vU5w7y2hUiQ4w>A3B>SfYZ0NJz-%G&!&jGfrp67g|1|Fu;j zbW6-KS04!51?^`_eO7o%k%Cj%4Of<}98}7)z7gw5R@3(PZmll0^X(9)!?W(fo+inn zHvgW|EJzdYEh9}^jO>&C5*5Y`!l7+O4y92k%S!O!d0Isg8mhWGj8K(bWnNqZn)F;& zbt5%xFGPZ~Mj>`Kx9gz2cC~fM>TcR*pWMJfo5^~0D$C1{!lgT;4R#{tb;;AL3b1ic znplNWqUh{|Iu;J0%Nlem_1TqgeErOQW-XR+R+*$GUWYe^eAUDlxxn9-MtOF)lU6xR zJLnckR*tWvDr{@oIh+8MZK2L*QYUpc+uA>Z&K+BNtCLd7i02{Tk{d%&-HjoyW!1}G zB{Q+WotG)pCfg8G-Yq!UIiV`~3Zt(}CTh|v4!3Dkr8cz-1Wt;{&Hqp`mgCe{(SAr? z{rpgSMpVv1doXw91!Huc&FgKIaE1OJ?Jc`@+Vpa=@>!+5HqW>dynTI)E-$22YP7Y4 zXRd~A6lz*-<3@|EfMSAWRs2X0sO$!Xcm+83-<7w{GM``a7xScB=-%K)p9#4B&m-Qa z-CE?UT2Q4FV4j;MG?sWZmn1Wsncb47a_x+>{-hZRB}too-Rl0MEuS0H@vS?Q?B;j| zr;S`L&M@_%PIt6dt#+iit+Q*xvRjU*)XIz^V}%e&bRiSg5YtfGv#gPy++A)))KEKnW%tL?T6kNlh$5+AJ97z zdS2g zrCw(C&3%W>K4>kgi7DRgg9Fuop|p2|Zd%7SYiIH_TF)G=r)4cFr%S}|S3Pj5Q=_G} z*BqPddeIK-=?0P?HlODNn6^8w=xyYb^G>HSw`=XEe$ss zYs(0IoCPQ^@?7mEwH0nyQfwT?&JXlir{OdObp3#I>U z+nL}4#WH5^3DF5X4tiCK6EdRRGDhuII=oJ~cYKwI+$|)fX(ZZ}?=Ep_JkZ+Lr4r1N zW48qHYJg<=PLDFTx4x=dR{X$nuU7Mo|4u5*J{tDCW@ z+YVcqs4tLr6m#9O#AGx9P`#e`K)QkmyWHit&vpHB&rj*wZ7{6h*c!`3J#_meG$5kW z&Nf~&b=dZH!#FQ0h-JBvgNNFAd3O>E&kiBh8VUb-T(HBP4Rfyc(wcTT_ggf~QhJS> z6=}D1{n2O9bgI+Mo^@JJhmY*^6A@JIO;)Yv|1NW|yyDsCKDI`5LcXLG&ENaxXYbC< zZ%uJigq+BrRNH4k->t{`JV7R%8bdb>8E*b4H?pDTg)U6H1TlYqC7? zTqgi3Gmc$lD`bC?)PXE>ThDBRb%3%jq&qqMbf{r=IwneCuX$^yv<_KQz9@G_P%rL^j3>;nVOXK<9BH)Ty0po|i96U!JiGg;oN#v7 zoGoQSnjW_wh@H(GL34J;+RwSzt{O8PF#e~+pLkNp-AY_-DoJd_@ikdZ0?ERV8};7w zhDX=zWq=>HyO^dI^-CX>;^SmD^7nR=r0Xn4$E3mIXdmvj5d44bGSk@`;ZJ5HUzrp} zYVwku4lw)9(3Oj@xwod!s;Z2m)wTI|+?`bw*G^eIiep&-3jsjDqEu+}K;R}R5UX4-DI zs7YyU#S)U(h!8XB-fX&ZXCD`rt;~qy30i7Yes>NV%{q?4jI*}Sk;3oD`;r%met96D z-2+iQFWP4&1VyvU!<~f}Tb@O_f9b|*L$yOYgpzN{*TgECvOjwa`z#c#u-j4R{2WCK z+p6q@twe=5#=4+!5<+;c?lf6Xh;ViR1AyJ?TK%Uja7SWOC$xC8jmYPc6IGa(dJnl-5khxcL2{6-ofD~jrCP>P zCq&mN@63Hm<+FkCxrBqiqj+!U*lNfnFT~ zaPo$_Gm$|yXueaV6Pw%ny5#63X14eQzOLBZ7Up+!`HmQ9XzPic)Lijnysdz+M7G27 z+3DS#C9VUrdIp!Ndm58D9P6?gX{qS5&#LxmEGhv}cwa)Joz#l9HT`yGr1|WEaT*Tc z?{fBmPVa8JwETkF*z(g#+X>Hg9dq^cyupP%?Ut}tcDCl5Lpig?k*t8&)x0}LbP%k} zPA%QNHY6+LcJUb%jZG?6r|z`}{SE-#7aRcHA2x`f3W?sSesj)TkaX)L=jJi5{eTcv zx-CI>qH}YKJ_(HhkoDG{AUfsWG-rI;`C_wN`de>gEr&^~g$QvFSFLc=q5jsj!re<@ z_bIgaWa_S+*4=W+awF<W^xrBR|RcS#DruQ=1ht^%B^t(e}4%XK}n&j$beg=*NtK!@3zFic2-Ojv}^EuGJ(A`hnOf6ooCYNtIUbaF*ht`|ZiVNTco}p)6 zQ>UZXL2Szrt~N2f)Uw>j!W@QtaR#=*Ezl_L$MRsE-yL)`FaqY6+)PMnt~*^lw0DH# z0Ziav8J*yT6yq3qpv%uh?oM=WU;XBy8yZ;eHqK)nSy4xwzfMKkx?>jgUPOHzpCR_K zs4cGSZVfYm%f3{gtHUe|gyPu{EO;I?Mor#4E5FO9t6 zjk-^Bn*;^v_H=Q2N(mLkY->I5q%f==^&!)ZLM(_a5_AnutL3uet_7X%l zyj%6OulDPa3ap78dnTZh?T18vZ_oLbB~AZcuI@bWB{AFxZ8AH}W1(2{Cv${tnQd!> zcyiaI(`tmJXuh9vxLfWhL748G(R`7xd1?$iG~9QRz%iOJOso zoF=wL^}3@HJ-)m33%;;2<}14-l@8Ij`e=mwGC}R9H??n8v3t2}6+Ta+L4~#N+~z5} z?h8GtW&P-v8#%~L58Z2`^%K$OaS5HmueX~%^^30`(9y;^m0yJ093~{_s1-qGZqA_L zw})S2{^3xZ$#GEfojKlOY^a-nW7zL_K=aM+(|DTLGl{`jQg`y-Kuk97JATaE*sE8* zbMBJ#&4j^wa!1vmJneCLq?cZ{XHONOd&vJVzm;hKmpQ+o-=#k#*+HJJPdt6jIih

Z0D^n%4+{o=8DU=7-Ti18WuJR7d5mbfENo_i<;tjBWlB@U=|n=inILBJ!Oj|dPrF*;=roQ#UIsol;| zo2*bs1s;k9mnCifNqU)&LrHTj%m56$#I5CXGO6f}>V$i^C#JRTDbum3-gl}uvR?=k zc-2e!EU9EdtqVJiaezy^6*NWBmg)HtqnX!X+85$(A{5;}!3K*M?}c{6nOQ^A!eeMq zUypQ5X{O^_y%_vC0}!}#o{OZov%>k!kGMv&DA;+^^db6EHJ|_90HDBYas_sHKj{xeJrvY*k-TIlA5D_kjsl>93W5-@YS%qs{iVA`Sb>cIC(ZgDQ7157%4 zs1y4?^TTg^55YIOzlEI?L}0pbZZvyQg?=UM9q@M;t7Y7J1NSs|#w`O4xO}Uemuj|j zI^rS0fdsYSO>`ii6+Zsd!XX1(xXz0QpHy!B9ojZP0-l_m+i)pSCW~kF4WPnn-yAa- zJ5_Do{e29u$1j=HF3!nYxD4X}_q3U1jb4;KX!t z=;KhDbWKVWdK|ReC5;c!E`9^WjLd`PbC}yhj++!vjI_!3f|{}DK-u3py%i;Ujin86 zVVTi<7y>G!sP;ecXI5Y>Ezepd1TU@;u4VG;igVd}%u@#;#8kI4_kDYMF%?%pH zGozK->EFu^ONiS0<;A0IX)~eT1TQ$GUB35Y0s|qwv9nl(Q!Z_E2M1w#?!tsGM5`m` zjB&9{DqrGEK*S-`;O!D6crCrngD2wF)Q$g`?AE7m4-WFvc)_{Rsz5dnL{s-g(JYvG z_e>Bi+Z_A_Yl$}$)o0Kt}Bk zkmdwgc+Y;RsgSid;yOtFSIg%gW!RJS1h)xi-*GWs)z(Ha5%!a_`2?=bOGIi%XZw2N z2rRyxH-7R{^%L$jJWGr*y?J{5Fr!ybnC_nkGS~u@_WkbQdcT9!zpcg>9^=y8XgGsf zw)&sS0B}4*<1|OI43+pHgD^aEPfQhp37d9>Cux2WId=uGt7+@}w*{Pd@7(-Y>qpkN zXNKPj1HHK}`~Oq2G*8)|h+gC4MjDA!7!yoj@K?F7t!L}}uphc7Iv}o{?vsp#G{S}d0%(;tvvp+!)j@8L~=p`4@ z@4;6m6X+2$Go1^d}wNJq|j^tGQvj>fVl0{v|I;tFjAv%I|p*SzNrLF z4U9pm3x6R7-nHiuiW+Vv8>xR?fDx~q()E?Ct=(|*-P=S68~MSbD1?dD))r3n# zr`rd}knZmA#sD3TOy7D+$F##}z9F7Khim3QCA*+`^@QQoG=^|V13?M;hrPl*T}NE; zft*(aovDDtt=z{!IL=C6HR6HYI>8feH3hQD6CM0|7yyV>)%@ugXV_pYxj_^Tp4aU> zJ{188Q;gY>Xi@XYfQHU6LcIOg1dE93@60$paWmpKbW~RTj zXhNhuMGpHtIKwe0;=O0E*gu0ckNuf-ceFehO1TI`UT{0ZiK1H8~f5G7fEcQnKoq|yrJNK~t z!UK5sR12halUA^?>orLDrlM`oT?f6>gah!NnRK4k6E|4H9Z^u2c1}7GL>Q{k;$@Ok zLE+MRxqz4wEG9S=|?8b*ii%oo~lhM0NHHFeshS0sO1fe7pQd2FCM%9qnR$TTJZVa*)= zEb2AS*V0=3dySyBR@yD~V}iXAVEwr)EOcIgQfZT5=;O-+ctR{;;e3`cL(gk9;Phi4 zIb_n#T7Ay+lVERtFu@QG&)|6Mmis^HrQ}sLkO4TR8CL$XNx6Hk$5m`#KsSHU_MCoxA>NZbVqUvk)jaqJ;$@^@ZWpPON^F!y5q>Jf z{yVJ&iv~2Rz&Jz-EEocFp6ZP3Y1z?+2gO$TXTg}v(T8f5)N{taH>())Du27xQXAHG z6s~EOz~i1}pcBU(S1#}jQlg)vCW3A+12OkZU^+AQg)YG6fZP1HxPSonnn`ixw}Rmv ztiKlU-?DNai~4|791@KdsKM(q;~Yf^WPqMezQ~9)C3gT47NkxbtFtCpNOgO!Y?f%FxXd0{&g*m6Z;!<@OElWQ zw-fFV!VEtZ1i~S=HGYt}zy=oo+fxQT)w@`>BtJD>h;x%t@|`a<&x4Skj_~Al?N> z;FSdNR5*HsNT>xCa4%^sSXBBU4S0BmtF_VfZqKW+7vbSqkXvGOT)+kTH4WiMVrHG& z>z1b@t`vVIX4cF(n#J2falA{ys{7AF@~G*Sd@?$=20;x>+=b z6g*>p{{igkm!3sDSdlwCnz2Cn4eAhXC$6idN)0`L;=0@@x8G_?GRHjCKMVl7QAqy5 z94P9*zrrON# z0qPT6J?nlA__n4d9ssEPGb5&x&-Q9tdzvo{QLUY&K=YI{G zIbPyKG-prhcdsN~10m>~%hCFC!t)9%LHO-KjPx!IDsaW_hXD(uUp+lP_E4E0NQhwg z=z;8VAPLNqm!{i%Al)yR{zkjDdZtCm1{JsvYnlVv=dKOQ=2nk(2>uT-i}85DNxzKV z!c7?mf#OhCYhkCOmGDC+e(1;TL`?xJRnJFTbNwL&A3Y5OT1xZskj+A{Tkw@SW^qO@ z_OR3fsvFW8&XAm92A)2%b1AHgqci!Gwtf&w5%w~c^%(x<6iAZd4!$^{zJ}wkDvCai z>#kO#3Yhv~%vzQoxi4?%BkG$w{L+5Nt=^6dYKSa1HLNqzT}?|Ly*CKf3`LW2eMu;- zXNgesYp1s?cY)>0JqNw1JwS4})`sOzuCmgABv@61-U=Mw?k8&R$+j>>By=s5y`cEk zcQdQW$WZhJ#>|4-uWV6gr`Y7TX%80Ed}~`0V7pi#70e}hgi{4(ze;(_g6kBx@)G(5 zpIGOGP|2T-?ie(%-BMvv(*23^}+#BVfdZYGMQa7{!3?vxfp>hfbQ5xxR3 zn_t18KEAuD!1Y#b+`mP;z#}hbAu<1%LKjcD*d;Ju_US^^l>Cad}I>2%IebM_(*=NhDp&Ox6tH z7!cj+uQ*|EnW_{?Po zj<0umI`$?^1AxByZl zZH^|6s$z7l@CR-{6nqJiY#50y-ve$Sx@DxQUk%GtBy#xkXamp{M`8>+6!R<7_1l`6f83uJAW*xOlF#mQ;$42Dg#- zOxob_@@1v!+QCEejIa%qSM#NlBflb?kb~9BejI-<(LYACi{Awdf9q_wb9-1SyoXhG z#q4NheIVlG6$}lt`>j}S3B%m?u)5nkfO=Sn%v$Of#aSnaVmTc8p!5oBE(kcv)IH`Y2Y7(H)Gd<%S=^~M^Z`B>qwx4^fWX2ul7_Z9c^ZP|w1uh>NW z_a49*O*Ndi&FrdnCbn-#3yxn8;N9IX160%MargYAZ4qzzUai)&!Q)lIFcCWSD{a*%Yk_!m9{B@=J=b)Rj zE(YvZy9XF|xBStbCrVLyOxFGT)%*W0hNe&l$ejD%ql3*a`x*U_zub*HAKWRog1bCx zZjQ7xyik7S3^jI#VJr8G1yCong4=gy8{}QOsL<7h4N$$ui8iv`B+%$6e{zdq%;txyYswL35I;8Q0UI@%1ICV0DgL?0t!xT6ex zr^6rDk08jAWdVjha3JA=Mjp#q_A`O--|T>DjZ3a55gJK*3s|Z};37ER0 zrS;9!LQ`k~e$22H(p6CEJm=I=Qz4JQR@wV}S@_MGZQv9Bo}GU!;my!m9!^I8r=wnJ zv^YFGj1|Lrr$!e z%)uzFoz1p9$u+}5uq`)T(u>JVN69h6e=D)mli=G9MlVt+~CEw z&Ty? z$#5*&K9K5rL2O%a_dXErhLBro;oSs=+x+uB@-@IJ=IK~czl9Kww0@6m(;%nezjI^i z3W|HovH`WU*TnX>2D{2iy(SRjth7OI{q|c4vTk|BfM`*MWF*?*-|HbnYg{<^Y&6eN z!`}t(j$JRnfXOC*U8H|uK-Dch|iYE_XxQ4mYNp4GRm%)x$khYgRD!b zsBo%8y6LPw+!v5_XJ*4;A(rG>48K*dB&%QVv=}>s_~6}7dugCu=9R#|tY~G5xRvXq z;I(t$Ue?Qmx)()qsRa=JT@4Z*gik5%1xPNJnwvIuvtAsb9_Vy16!}o?P1Dd+{{%bo zaa!(0Jay8*)`kDunmWX|8kz{%wOBL(FA(-`9r9fO-4#rCZj?%n6(Yef$o) zQ-`Fnj!T@ZsbRbs$8RABb@LWrD{o%a8AbT1D0z1tV=At4NQ)1Z0XVyT=i+Y7jFW;( z=o0@P2S8oENZwgD9GUzyf2s}o|0u1i`dMiXP}tlQ*{vftsgc^%9JFTlf25+jq>{)t zxCc})@U?&c9^4oH=GvEGsRKRS4-~p38T*KoU+aG=?BC7-^@tw~9~n*^2~~ahPjFZo zdXDKWHSQply8CwpkZzzha{JKpch!-HYwkY*Tnda3{!AT5|GPT|>b9AKKLlwvmvBi~ zGL)86nybb@Na%amh|Q8=r{gW8ar7P`4fn8d8ia??B6oxCq`3yq7Of_w7aoAL-zI*ULlzf&Y%^&C-xu*sU6ruyTRXPG5M_P&o-g zs{9&zXTAo};}H_{P)h>Y?fFnyfEc{mJchp*Si6CkbvWM=E;+&xsu1sYPpp&`t+5yR zO=7|5B^No#$QDj%%0hK3+W}1SJGHCgk|p#Y3-1Yi#93MPp^h82e`YY|Ewx>+DedNj z9u=O#MC$M*r`&MY-TXEUzHZ2ycsqi1#PKRh=N6)j$rE2#3sD8JIRGO2A4BQ`)>rd) zZCP44GdJ|E{o>+k)Ki-y*5oCX;+PkQD*DV`=@eP^gMfJ|=T<6!B2U$D%GJ->~o^5rS?#>gSynCeXfx`4f8VWB*Uhf2yx8`yl`s1#!`#%bo1h<*OHzjg%JXpJ{IAN&3x8xCO~pq@aEko zg9owF9)~1_a%;fwj|`w{X@S)vv*PU-+NW$7p3r{!SozP1@2gtsG;HlmSm;D~JnK1-+ zzY+j$;atTzH04ciY4L?dvxI@jk7Uj=hJ2xxu_J4)|)F+-Dl3E1lT-Ju~pYu1ZjfO6uGL$-mseHqsY zTMJCtfR|7+v1?i-VKSn#gB30b4~E%q^JMIszlY%8bA@zu=Ji!O!i7nK+{u5(aAAmE zTKoZ#iwUehaKX)ujET2LnaOLZhjIe^Tw1xhOtUYQNWa2H+S}=Cu+KO{rR=p(dzuSF zX}P=SZ`bddXcji&$AqEXEM*8)Rsc+>V|v8ddybSa02%R#ELbU|p*YeD1Km+?W4Chu zwO}bqO6HR7jRrfZ5yN-Ot`}2s2l-BzfaMBa`gLsVC)8pze>;KWa(+8WS-Ki9vBDk) zmaYzpKdhA`uS@sh8P+uTKBOY3rsSom1|#8ipyI7JIIr!8mgOHFjCJK1eov{iUWPMd z=oJSv03m(ph(SRJf)6w6gv2$0)_Hqa1pr}tD?gDjVWqO6c1_(}#0SPQPGWhr zAt&nX(p;L4(+oEgb_?=_oSTVO<^ni+N5$U~^hXz3DIkKa3=)4&5vCSeog-G>{JMFC z!gF64QLS6@svC6^To-B%JjsN{*p}W5pb(k;9uln` z0Q(_17i506Z^t%u-G+8z`$dS~+gZ9h=}1`Voz(?|cSej_<#kE=)&o}ObK#Ok(oJzJ zaFxUY$dh{+Ub-!P_FzZh+1GGmkli@bGM*IUR>2jm@Gx`;-Td_OEl4WcjCuT|j=|Ao znS^}#;34lb@_O0mxhtjC;Nz78*PG0BhU^rm1tX-|{DWr` zLic1hW6M3>6#h6ZUBtZRQ)?pigoGf4&j$qEmA1b^wuQF?jT!zn-B);|kPGuuEB&^D z=2s$RZ{A$x=<%7F;jwQBTX7%?ae{<=T)z=-+9ObZOr>8 z7)}c-6>(V3;#PFge+@`ZTURoTXxdaJ<{N#k?NAPUnVKnhvOIpuh)(O2x}^}DM0EV( zmSMoS%FT=#5O{6zaZIEyz4}G@0HLYp;Msy1x99wDgZt%f7iKWoqhFp5#mG(*a>nOlfUkR37byRZK2+V6Q&k>A zNiPFn19)pmZz=;Yivu+VzDs$Tt@<0?M+2zD3h!3&&i`^@uE3#c_!oogKeQDF%mMxc{Q$zEU-Y{z8_M;NR7N=I*j@k~Gr@`TW<;3h9=~ zmskxlYk}>dXYLjsK^M&)RjZOmSc&3=b6Y^w?CW+P2=5F`mr6>gk>UOXMSJ>z90Sv( z&KKu-VRVQp!?OYy99^opK?Fa_=HRT;xg(r$-SU?TRAS`4Ydgbxp?fozk*t8iu5Z_R1r6j4$om@fYj9tglBai<@lHNbDY!8aC223c=v_hO4#eJuhv5oso*6U zS^-3*jqo`gWN<<;DRR_*-~OC|8^JR$FZKT%2 zo@;0>?K~5wBZrkeV!#uYE+uU@HRnlVRBG5K0PXUii;n@dM>6RbBKWoOg##ovRn5Zv zhX>Dvx;cwXD{Mr5kAO0`xl;dryWBn@hrNVfX~E>>VjN%&E7aok^j)FVa}p~VGoJh# zJ&pKsS{pTxNk{l}0PGTHih4Q6iUkIG42;3ctu3iOy1K$0^gvc@H|Bev1CeGb@KDSH z+zoPNt|!e8d2Znpg9n4gE`8+Qi+2;h2R;l~S9X@q#;-qvx=_m@AZ@pN?A{)|&#$`i z4cr3te^TybCHs-SV4-m^YVODqJ>#LSz zwQE;j$9QXyKfd^r-$MvJdL7Ffk~C?HjyaD0RY;TeW&sRjw|(bLn3_SzRH0f0s=HIl z7i1T@6$#zQ-F?_~^lzT5_zVr4T*bGE6`(G+)Zf%>_Pd}_YZz%ZN#oNKW-sH zY1X$F4Dvdanc-3Eii|Y!v!C_GPdcb$-M=9fe!77%`xaHM@nH!oaK_AHs*j z69hZAQ04}xF8KiRceo<#x;J0Eep}btqxq3xg^4chR3L5iIQXrEn*;OZoz9KG9qB6G z_b)cBKbAx~swzehZt)?)d138uZ%dn0^l&vs5xfUzzoQmagX_v{HoG02TgkzyvFO&zbRDHi7WbuC?D7(@$J_wIPIDiM5)1Tv2<>I9pfBX%18_H$2RZ#%x>iJuQ_9S5T6aQ;8x}%NpU2!0 zKblG!F45+<5@LYdhMp8tI!sn7$h({;QYv7rn=h?UQ6MiZjQx)CTD}VfO77#Gg%&i-v?}bR z4TqC7Jl|S0$-LH`+q+EEW~l&QVr)G>)h$1a3oG1o)w1^}M~3QzP{R1H0?2N3eVeCk zpFZ~yucmKIa02?UOINb4FXn46e{7WFS5~E#>j&ZY893uI zUPlQF*PkWzhj{zZXO>VMD4t7Wz7vJYAM8b*k5d)PCD8=sK3`DIae(0~Y!=$c1`Kji z4$~2OMsGC(-Ib}tWxK69V5r5zUlmwxU+ZE7V_{%4L%j_6g*%_0wmPZg19)3DFbcm1P= z$Ab8`e75UT{=f>>kH0M|P%aNng>jQ=z!VlH&--KC0%Z`gW5a2TAhe!ZT&rJ!LYu{l zbSePc(rRuoA}{cauFAavzm1ZFHzq1M5-`;cHxhdJG%}I&XjPKR3U5BF71~T8_@d6j za4`$DDd@Sqr5*yJwZWn6(}R08dj%is@rH%^!$J0ps0t%pnb{>A2e#?+4o&I)$DK>r zwhjJSulE`}UH$w%P_K%uLGgCC1`ORXE*P_XMcTHxq=#P3T!Hx#bfh-*uZ+&$N(`RI z&I1a}EBN_)hiVEWMf8LeJ8)g+f)#!Muc@iQajnP!t#r`l9*wr4>jj+BHeqm$u4u(Tr;fi=?JRpDYUuEvi@7LQ*7xM6SHPe?*f3C1j zPNk1RS_UFYH1bDZ13g!rq2Ev$ynTJLW^W+nD(~_A&)HH#J*M3VapJ)T^*vnR$L6Yq zKf~go`9f|sS+ttqyOoH|3xMOhPwPlB`xV{(W`L5b!oTxl(})H&WiBojus68n&X)T7 z{AC(Z^E89T{J#qCbSMcd3Mv=VAKB779H+ zo1}sm)o7vDV69Ng&1JrTU%+CS?^gOG&bNTE8i>W7N7)pwRmN#{LcuQ#c_{v=!6A2W z>e;Ti2y5V#za`Zdv+qx|nW1tJj5V`Ys4WQ+E(q3&^t1+$mhz#~QdzF*{knrM8$jB* z#?1u6+cw}?%^~=J_8O@2H5}EzW%)Ab(5x1fO$I-?22+J` zNea~bB30yiUSd;V7S_~XLRVT&c5Vt~KB3?5qCWIR60v zl5OpVd3HOVn8H0Z{c621s+vL1E3TYJqR?xPtxpgL$9oF1FwVbojMkKD)51ysw7p(* zCmrS=Okv+Mh_>F$qjbWly`ldGJN++7A}aLcpZY=uyj$4-zvzCgYabbGN?XSZQ)(8q zQyxp}P^5uv+s4d?F`{18ZDoY(VO!as5|H+&9Jq$P=y&NHW@zwRsC9+w*kO}tqYFNt zT0M`y!7^dxohs=GtY&gO93V(G>+=?MT-g96|0<6UFS&M_=N8aN{Cfp@?I`)gM}-k; z)D!F#8$j8h^=}^_c~{$4{}BM1rQl5c&Yu<4fzBMr-&p{(GPgX-SB;ub*++cIh7Ei6 zMI=Tb^4AioFLO`G(|62mLqU%4-T<170a@iOpJQ{-uFL<03oNyA3-dO2)8UCP4VJil zru<&Pb_=$a7Md(r;_9aKG1saS%8?qjBfzwC^qW=B!e1CB9c}U-M9}kvxTC2 z!V_w=%dAN}T2x^cf2Qs*mn;sr1FG$b_dsuOQi!4Bbjs6YJ|fWMzXu3_Rz{5HE6Puo z82Pd$PGbXTOD)gUo@#ihhLWK=US=ltaZeG#r=-@v%xVhQmOb*HscJX{IKnZZv|H|6 zZUKzOU*{E8g3sUDWt1ZdN>ZPM@CWKnGkUowxa6=Ff}I~YHpOp$E%3#{+<>@fS9LHX zT{4k9PY&0Fe+#Wk{zyeKVEFEP=|^4$eeg1s#2Ywz9S#e0A(x=6Rr+rWj{~zJPb!+H zHJi@;JB7az7GiB*J)E2F;U)Z5q@aCy1(61X$w$Au8mk*2w@|J>X%-h1rXd}C+`=iA zN{U}9z02$OI^1&SWk;014M=nM^8OBwg|)}ESyl>Z`2IUW&|(z98y0VtyEcYHB0KfJ zOzBTj;nr{neG>lB1Ji9ib>k6GydyHRgpTab z!#rN&>NfCt#TBM+_m-SFMYbaql?h63T$EWC_K20u%Vm=3<)7CX62ti!{lgRA`^ebjF$Mmk?GCY3;m+J|7{go zm(1ZmTyt9J>oQU{d20`wuj0Q|nYCZbix6m__^a=l9Um6sm!L(gFH5%IS7GZN)a|apvAEf1WPh z_@zHnGXJpy^o`HHpDE@tisI_UFdg*V5jZc0^WH{B(tVrW!OxAR_k6}_!5hH;nHGyi z{St3htXyxNJ=u+)%Y51G`;*Sx!wmxOC2NF;ijS4civX6qtW{1^6utYs7bs_g^#+BDuc2IPgMjmFumc=l`n>rwM zy--R?S89L!@Crq-=P6`)^2? zFo85-C%$HIU8qDkZ?=I56hmPHh1L}T=AKiXAl%Tmtb00T2N1ly zm&`bWDPeyds^5v~cYV<|VA-Z74;*%1kaS}X=0WeR3eb#D5gJMN^Pw*pHXy*m8+7+M z{}Q!6V#1W>;o<&1*B|REbrKXJgv!eNs|==Y8`;=e<$qmXZ5CNu8^wawu6q)75;quc{Ncoo;#_$?hQO z#$)nLcdEKDIDmKf=+4BzpqLk2@w3DZgqQO|1%BD;5)|Ag`i6&lpa#ABhGyxC!78J} zXh9P9{(Eh8l4-2Y+{+>K9VQEukF6Dz_ zaWjP5V~9K%#T+fmCG&>(QJ{z0uN~0ZEdwpsB58xDbO6kykz29r=B0LZ0^riWm0&e| zj#b273A32(+t&dauE=%IDWmTHWtbPKyAC#yoC(Cp4^juI9j>MoNoXhOojfF;PkUzw z4+NlTde*_uQTRubjy=0-ApcB!3cuwcL&aDK%N>5*{HxGE+N-A7R4nzYe@#Hk-JE`F zTF}5zxrU7a9=SVcO53|I1W_l9Lk3v6W70)g*!n3(9q?38+49$mIUZD-O9|HldOOO+ zq4i<(R*xbKuybjzTSDS8#=ggz2h0_Q7smBBPy(!xIsRBs{Y&0YXuPO?MI(H+aLp}w zg>Tk&>gMH1$iwUvXkS)7k(0hB9Dr{*+@B-5*eqk%!?y#3-L=XE;5M&14t%5m+uU*y zGp)y|xBISQnAbvS7vg?Fku=GfSPg)RAE+?E;&XAiup~>#gAP3cI+%x-la1_3kY7 zM$+kDcpgYe>np3w;YX|_O;7V0Ky^cI>2X#y6Q{56(t*|6I$g$G&0k*mwp1skHWYHuy?!ZNI#$c`m$m z=@uTwZV)>Z+2!8L#fQY`bHP|?bi6lfUa})ZU5G)oUkH`a0WkE3VL-uRadZ^E+&;OM zbvzs&|7wEJ8yZx4$BcfR(PqZ+$aMG|pK; z*1%ks0p7<3vlKOx!DJ6VubmNFhh!N;RF*CB-+nuL7AYtQojF!}0BiunZLQ_LlWk$p z+jqG66Q+l6YcXNRS(6oZFd(_oqkXz9Sl=1!$OaP8O3wG!_X%#Ao)bCXxG(}2Yo3E| z^=R`poCC3F{{SdAc&@(ahD!aD>L@ty3_!Wj zkg#6T0daBpgxYD9}5x%`R#!))<1L0 z!~sc-u3-$^!hJCoX1`!=as<&^2|vKwoh?Jv!CO$(Yuto3Ou0wOr_u3jrI*j>fNO4R zxe{KaXBc{SZ}~nR+l873(Ugac73x&QbvgKlpV|paxeem`v5mPvp8#A%$wDoZa2srI zzsb(G*p>m%hVks!K<+%b=AesqyQQ)A2eeB&B@$?AtOD0FAg&vH7r}(jkz#+0+@qLp zJ{GzqA1N}XtlJw2&SIgQEeR#;h8T2T=S&cH={pk*&hJWd7+zyKOFds}K6g-LMEFK} z!asL<*?Id&RaJ512n*3j@0=f_IhN{Q7F^_}c`o=FEv++CMx_(P;m<^Vktn9#C;<#> zIGhg?7;jncf4_CtRGub8CU$@{Qh9oVP4m7gkJz)vx+jyKCy}GF#eA z2`pvt5J4I~LD!{Dmtj+VjxztAnHNLrc{^Ld4X@Rs8B;M|k_pn=ma0F4oE|QO$ifR)35_!yYeW! z7ybk34=i{f> z6H#ai=p5&{6jr)t&hEh6X<%+*-!7wWgqj8poTwA!40CHO%*T4!w zD0eutPZLOPG<{sJ%1X*d9ekH?Gg0Gm$$gf4eTdH8-y92bowvFZZu6~%`va3g&izF- zU`)e}JP14$%l&OP{j#-GbBvtfT!A}7%k3X^Zi~V**bz^Fxx{G>h8l|@8F00dqnz*zXR_?>}?R8DHKd&0v;jygQ zjn}ZhYGu*0tws-W`>2Rb6tR?vVZa(uZv=G7q)eV^pA zJ|Q@2VQb;vpW~vpJgp2%EC`=0Lecc^`Xom52ic-nuNk7-DYt7DvpzLf3H_4g zqRn+l9D~Vz4NUhr%b(rT=Py;KRCV}Vp<)A~tA-Mv>;AmwgT>8+OT$S0du6j{4AVLE z)e|JleG4NMqap>jy!+);aJK}M{17{|VG-UO{ziz7W=i~I?T+P-93HBjit*Nf`@F<- zUAd4S{ct5FqKxG@`ai*f<^3TmEU2sZ)o_Jd-o!#>uTtLL}Rhp9{7?+GMj!|MX)W2IXz zmRZ*SDRJJdgu1@-B`UgtP`TnIL3k;tLQM0w!6672#BNgOMR#M~#zZ^Zmx1v&Upljj zn8p~+Irscub|9#Oi)ID~CE|o5Ljb#vGT>LSu(qBv{DB*4?vhQm__0z_0omWeRjgw9 zR=Uc$1=7j#YhYZrYVNo#@RxRu{v}(0b=5EAy({4BZYx~Jn3&~aASd!Xt)-hIOfQ%) z(ELnamQ5MB=cI5s-Aq<5mwCRG;nY00X#vZPt`>Z5&#I&dPka^ZCL`@k@WkPPHNt~4 zd&7|#ps8kDs{rz?+p=V|^)cHLe4-dW?$Q6Hz%EVyV)AJRhbRkQsw|Lp1FlOSptWW~ z`q}dWtQ*KQN4p?AVGudPCE>d4`)__LRvBg(!L@dgd8P7&3oQ~t1u#9+Wg!`@?RjyQ zE@tHCg<1awpoJJ|@w4i9o2?5%xfVXGT`pkgkdyETOv zgkb?6q#7n;i!p2T0M(m_$>vx&J>tE9@~)EWaK^*nX7&io0+p9LUyL=oWqyR1o@GY6 zslAXqN0703`+8yl>{UW#V%9H1aq$^g;pUhRLnqNgJ>hX!&Io_vuRIkgWb!oA1^zDG&a-ALiWSa` z3OUKjO~N5{=eKpss(f1h9IO%;lyGq4Tp;jj+Q%n|;zaI@MRE1iwa#+UR?H^`~Lf?l0@AB^VtD5%c!%%@y=@yMF4i8}+wi}1blMInG zi74C?I5hX2J3(8euszhX%!r}gU|zHK;}s7526mL(k;FYmI8G9;8&)xV@5Y64)6M@V z*!!(!0_lE_q7@iy&#(|Syuo4fZMN3!@DB6-Whn31xpZ6;mo(Qr{FRWY^*rScRkMsw ze9^q%!W((%Z$SEy%uY`fS-^M;_X~L5yA1_HS<}ob^E(wC0xg{lgI#HQ7VVmj3#!N>`vf-1F z(!+a$$h1H(;j<}Mi;K9k0qWg5y)}%(Nq08yOSAy>4o)M}`rq(*)$kmVC_VW?qv2&* z)A$BpY<-CwT=E=&OZ!)K7$Gwg@2HKec0sWqf+0hF(>IFV6Dgs8kHNoa!z>TepbgAu z_FDt96yU9rnf<-BAzM}>D*a1l^xf4in>T@+JZiWh{fN4G+X)&CF?fO0pp!9Y>_Y+=rg6|M+o1Jx6XZWso8Q-lS4moF5p{U;>|`W&%~PIm{WffR5E zjgN(;39IkZ{zP1z#Kgq6{|iBBBN1cjPz*6?HI>|)RS*5p{yS#-KuaNt#MmoMjq zKZZ|Oqws(5pFb=B7<8mCH(0=Pg(mz-dB-@k23^Dgl*Bq$>O<{u zSeFAM>fybI?=H=KOFIFvl|5Z*J?;zTLQk!DfjPi}Ynj(rN+Q=F7p1M28ZOD0-i!-H z!H9~~dblLCBXrZ6X_l1^R>C!5#jsKhIz)Wn1^IJiq0TKgYo3k?NtlPP1qt<_%f`y-QiTE8I^&{`EV{!{DXM1Xq-Q z`|VP=;Jw>s9*xL<{)Xg7jSG0M2E?6Tm2QDBNeTs>LA_%g%z60`$6E=vfbb4){cxT> znD%O~=2jwcJXZ}#LB=*Q^dymGscE%exKkCr(mZDP>kv0l_T2)aX@rOWCd`*1Kf2U> z6Y=z+CI`?MR#`cXQEQzChX*n?`v|IxBMyTfWPb+XecOyKMz> zx8itaoBeK?pN{M8>8v=cLGr|Bkib$C`l+<-;@5OExBKI8QD+QX$3MDV#t{T?Bo;4- zlmeu=f%Q^CDpMAr(6pMb#x}^i!PI-6))NXZ&J0i7N2)_~>-nG(h zk=0LDzVsmb+Mw_T5d78FEY%qyFWlcz#d>dQWQIkV_`Sr1pudE@>?7YEm07s0WU8Yt zGY_@sRasN_3Xcu)t`OC~I?SpASSepT!nYOSEh7||L0#L9 z505qBPKCL?9N2u0rQh*}t>`39&H{HW%O)9lej@{nQeP5xK@=`?-x%Umi}*(2mqK^6 zIUiFN^F6KMZBbPBw{37bNKomd?3khV+Kh^~0st;FWZ!mbn@%1_q`a@TmzKGl)@y?k$QL2T%LhQa5*p{ib_R;K- z`E!Qdy@C}r)6ukaHxm-#?W>4wW(<@^MHKzsH8MHg6x~?j5b(_!>tKI{#D`r?L(=l^ zopDxDphWNbZ|wNhAn``3cO@qg(~}$eI&2lkg-K(Y4O4(sFHig1 z;Omxknjkg=w2n|lgQ`nAn`oi&*1|ogEH(gK{kx8tjdCCrx@{cRbod~AD0>NmTKRLr zUIGJMn(G4gix0VS7@9N8+6vl=jq>+q(h$B*Ylpk14baLtg(}oTme2HOE4aC3u8Xno z;+s<^(=l zx-%3{LfHn}j#LFgdK!Rb5q>N@bibUM1rrz`u7p|>ZVSOt^_bC^RD}b}b_2gH{qt-1 z6PwWX{O=8Fu5fz3l=y`GyvCXx;O*Gy6qx=66a~MTJz?rT(xPOal!ODH680C^nD%)U$)&lI@LFrMhvs^G`!v_oe_7bTon^L!zW`^I2 zKqH~lf9{{{aVH2^$+CgaNYB3g_!{txPajj~1avZHZf)ViHEOP>0PK@7qG5D(goV1b zRPGnCc|~l*45!NGKT+w2yODggN^cL?3cPg5x1w(vy=BY4KnNJ>_MDhFA4r>9=wY|1 z(e@1n+Q>uh;m;d?bCj>j4@O`7o_=kBx3y>QHT~t~EiV+=z;0Wkiv`}V#A^JS(rv|V zIr3S{JNB;NVJufk|Guj7=UDqqhf*&K2$Ed->?lZ03t;7Xx4~vh(i|Cb6i{_J*halO zD2916tsZ_WBCtkki!3-9ObKfpVI^c~Trae6LTTWgs(uSkyyYFvD{%$DjKe~>y039z zDi~&2>kKy*w5XBlt@)2<7DDSsDR_OTUt9tuCyI4K+5Jk(0-#G|m%Vtr+?zKFTz8w7 z?Y)Dh6%ba3Qh!M@seS5HhQN0!7C27-RzgBeS+pDg80GJJ_U3OP-k;nffv{!iG$#Jo zhDKz5di2YrP>~Q<=>67!^u6{aG}WdX0gn8wG|#bep>CThYSpmN1o!LqnqzufsrU~B z`0nie0uP$LCSW(C0N@SNpF?`xVYa~4arQ@H%mZe|7&ccbO`%@?Ir2F-I(5+P2~751 z2>81z_Y8)Ugzl3*+FF=X0YGMW~X0 z7>ul%Uh<$;5x~6M>PBH>N2LL;8tb8>nfUbClK18%!3!L0Z-zUH;=C-4*Z0IjG7xn% zJ^kZTjQ5pyT~D`qO`Gz=gi&C=E1b;CLrZ-S)DR?GkwGrW63W!d7V)keu7_0EOU|R3 znW=uv^AtxxhMMFfy))Drum|=j1HXTdP>Egg8oFSTP)qZV4iN9=l**v#M5UUE=D+H|4hBkYIGZExnHSu|)yTc?|{*wc^8?`TQRsOFFg29Y`xtb?Oy(aZVZ8~+v zSp7NHEClxd)AOpnn!`dsF)Paug?lEUhw*=2tA{hkCp;)CDJ*Po*5}+kQ`A)|L#<&U zrUA9*^>YW`uePub4nbvqhOo=YpvOY-Rg6Nrk<>ph0`8eg)7Ri**pA7 zY$-FHG)fI@JjfM3R>OC5qe$jIl-(zRjsic1UCYImN~#0R5bgfY1jgI*g;901QWD1wW(9t_^+hLnGUxt1oc?-q{PiMc2!k0n8q>rN$8KNV`gpC69SGbai?Va(EDHQi0>|T%^ z491lkUP3`JAfIOXv?*=Nuqz5{fc*B<#{&^>s*;p0?ca$6x^76;J+@MM5*DW4G!*&e z#wkae=!wSW-NU^Brso+EZ@5e(ed=@>3PJ^dY4>#rM<)&7N4(B?6R zKNBHpoBAairEC)pSHEe%p&=OW(c$4mn0FQzTQCZ+V11k)uUt{=Spp53BXNzp6c(-ZU@u& zUC0WE_8tTkw z5g-bIY!_}Q2G}XEE>8j(1>L`-1Ppktw8>q{s8C0oxDXhgzb*5PcJQ$7{C|55EA^L9DDB-5>KR*yB>b4)-pj@$#0}vW;o?FrTVSJTSFHsn2LRL_|1?C zUYc3+DL~mait{KH8J)NFiv^xUP4!oB`=Ek6`_*ozHOMOQa3Waq1`6bBqtZQJeImVE z(I6@Zz2oS)<@12;Q1>g*J%tdaBbJ+u@Z1~(Oa#NJi}|7k8|ljKK?MS_cV9w_hdM`h zO*;rH8#cO(SZ`!?wVZDL{UJ~x(hOD7k)vj)w&GuQ8h-=-Z*WbgR?60;@JscyS}BoB z$`b}T-jggEnod9`Y zoZ!@3zpWwF)S~I468=664ZbF^%ul+#CqWM4dE`_dQ$q_Di<1;$Bg~Y{T||`%!rVf$9-7Y%Ge(E-k;G2UT+0>4)#dH=nK=l}vdp=40S*3!SIRchF# zfPJ`+aA~Nk<>g2^Ed*^Qw@Q_A4bBq3e;ZbUWmP{p z){sz>F<<^2nE-!7s4de6n?arM{4)glJADJnZ`-3@EMX(AoS3$iPkyN=DyE9b{5FPk zwblF8ZcH&CpFHwgxWVYYxY2iH+;H`6GQp@}^9_lUk5Ch9T3CrZ(7AI3RcEk*M)j2; zd>Vqo)}g6jqu-WzNr(t~I&J8R8Y(r&Pq3-CzhUc}dq$X*_fYhL=PMN9ONt-q2QlI= z9Dlvp$FSfjt=0%X4&%o;Z#kz%I1Few!JQ#AyY(C;(ux)fzjYhtd#SIQjJ_6Rq$_-U z!_mf>-tDX@0XXSKiHe(MkY;bSmH$0{m_JGM#9~ z_x1tdgW$`n>c9`rqQ~BPhCdJQP2HEVP1`m^2~QE0ydxEWONorJ4UOUdK1F@&3_Ox` zmV~gV_wzmi>PwUd4{P|RR<0cW%!wDLq*Qo@0fx$B!jCmcLCw9DEjcKbB@=Slogz9- zC= zaP#Cv-8y{@KU4c^aT7Zm7oG7b6Lg376Y*=!rz+8#!d+l{%XNgSfgv-q&#)*pQ%)M6 z133u(4YH%AP?uQ6%Lftm7+CYROBRqMoa)#6dwOA|A~MlVp?qXks>5ZdyEaA!dmk}Q zy_q+m2Vx1h??~;KAdFJDse4isZT@q_{$kYt5orn+kpK2J1O|M=;vVWl56}io9A?S+ z)cgpMy*uF$y5ptsZ|2@mL(K&CZ*O@r5-hwF`3nneZ@J0#elIqIx#IV;jn}1&OYhUB zvG7B|2Jm;TkynO7I8$eB|DJLAXZZ0wrvtlDN zoAHYO{1~%@t&LH)sO#=RDEc*6v{n2J5%YwD=|cBBYt<9=Tz#e#PuF zljx|CiPs7K7I{18Bk!gnh2Go34le)Bpf{$0H~DvoaMvxSgf27ZX06Kd2RZ}YRf)#C z=WBYuhaaplSiONS{X#Uh&T!#6?EfTKH7Jwtymgw&ECufLRXoFxm-FNg-5(7h^`IWZ zr6H85hh%kVy-}%!p9x~a-nVnyr0v5u;B07an-}t;AhR;1-r9Lh6lqbFj zWwmRa>AyTUHsqliMUx{H9@;ZpaU%R9pOI<2wq{OxyQ1Gp1W&BoR%gl~g51}5P4mc{ zxsp}GbTrw_Zy3OJ$xj0Lux+ryiaD$VgDStVq_4PAJw0N_;PmoNJ9y1eM!beUoEfCv zR@ZI2X^Lu5@tO_>sh8Tk%n*79#uRo%1$t|5U$KJqkxA*><Plg_JUUP01S{M%OWy*Qtq@rKed1}Fbo`u-xT@Oa4NZl zn}2N@NDhGt_137$L`Msycu5$?b4mC+)x;b%>>IFf(DwasIaUv$u2b$7*5Gy7xuybE zzG}l9_V0!)BuaDFjO5w${Piz)LnrM!@V7y#gc|F@P+=i|5JJXY7@}BzD@~@K+C8eEqC7x>`M~m`aSGbFz)_o7~Y{J5`hTo{$3opLVVGs)P+6X)1hAAarXaa_aeb(}~6Ip9K zvVca7FM}Vpj~^X80M*F>@zrb;XcIN@)h)6F^K|I0>|A-3XQ<2v6iK;V1*@N$5(8)G z27vCWW9lI-E|2dvd0dFW=WQ#c$jicx)}wQ{7yy?xFHN)wr*`N|k+#-)o~$z-TftgZ zScw3WT#_Mp3u4n+=)}P3C0}wK;&8wc>VWcZAvnEjwLGtuK<4Jy!AuJ=Pp5~-cPKD# z@yGQ9g{rSl=0rm#1Q!n_GmIUA2`TN9Sq2%bhp`xBUe0WQ6*Pl^O9>Z*IH~4#?K;@e zbg>>b>Uk;$4$!*;U?^cngSmq^1SJE+uG9pvLyUhRe7;wn?xyagNNV>7Cro;yX!|&< zVaUwOW2U??O4?~;3ae3*Sh<9f0>JJ~JJMAFtH4J0l^XKb6pcJP&km}MzYwO4zO8#F zK$5B7W++1p)16<nKnhZXSrc!#fb4Zq zQv5zx`Rs;Q&M^SpDoyS&yzOe@B?r0}<@3gY*5s$mMxZDA*>m-4p5^{oa# zp10ZBrOaqB;t`PG%&$f*fc`5d!>Mj2ou!3UhUV6bJVZtZ^-b5P1se+IV;{}L`r~lB zm8Ou*F;=Ci>*nGiqpFB590!u20|A-Tk2JYN&4T&ZM(S7VKwu z{ZuU&P1DGk!x{tgJBvI-Rgu8fOP(Eqsz6mqEBujYsQYEg%Y3`k<&xA<}c}JIXpgdZy4E`Qeg8B5$f^_73?i-=j?Vuq8a;uu~C8bHcw@F zBX=D}HNAxk;)ThLI7ka@O?f5?R~#^zZWnu3;axP0RoudP)JsPzZB|i%1W^ekZ_9rx zJtPPUFk>aFdG{@b(k#loU*4$3-7S>kX!S9I_Im~}-_cSl+Jb#b$=jY8SQYdwHJ{6X z38UGnEqsX}@vb05eSa75#N_EW@oG-Jy#YUua`jSKGLLl#cV|Y+nPBJqv1FN1JCd`B2}xTo$}TviJRkh zIHa&ilIf@8zxZN^&!ulBYK60N4i(Gn{_$<|Fk=Pct3AO723Ij!8h$`jf%Wd(rcaJ1 zQ@+Q0QgJ?2y1?(nu<#l_41m2ndLAx8rbVqY{C!BKE;(IGQ?Zo%IO21FT`$q6eIA?d zc=+=mPFnqv4?&Ojnt#h+=P*E<1jDrKt=2pd{gBvCqiTbwB*=sK zBaU7(Ged3J0pOKu-YS4)3N3ksp+j(ZJOm1MQ+RlXnF>RPM!Ee&{fl!B;+YCgZ?HA% zCng80)DaefY)9krq87Mx_xM*rU{cq`L!yF39!6&ss4s=_{&;&D0e5HiO;qiGWxk?1 z3A5<&R$Pfl;Y&Bb&rCn=yP)){EzP+`2MHFi>EAIxy(^iOu|}XvM=-qiU4?nWw?IdZ zJR7hquWYaI?9KNhQm=AB?V2CVRKL)$2*@+pl-p1c22%8*0x1zrIn@*YNQH=nWzY=5uBOWZCa16XSvJ#5Wijsm-0=7be>3k!B+a zDmKhib%&?0jvF&h#1Gh={{{`?%`5Ao{-{cklvKZe-x4rhk%51s6?$qarWuMJ`1lTL zms*!B_niV*0f;xM1>OaLP-@!o3>EDakzPIlZkCo9ND6heKeLxCg~0}NwFI=n{2MNr ztXm_z<+hG^e;{L9<3bzJ{Y@dja82`ySr^85F=< z1X5X=Zg9=Z(ki?hCQ&|~WdvhTAXZc$z`N8-k1gnv)-hoVhof(v{|--Pgxvd21g^gA zEifWQWx)IjKM-( zaa`ZEfy@*Xg7w3J3KQRP4bYQ$piav|2?-b9fKVQWn0CqP=XwpOFX!|Bq7K~BlTDXQ zKAHl*Rs55_9X|}$jw9U$4sYA}$43vRJ$NQHb8iUe?%NWR!HI}+v}O@(oHl{57+3q8 z9WL3AO^xblGYf#agqu44j1@ntCuKOR(6a9EQ(=#|De>njFc@LK6E+Zf_vMzVksM=C z8va0}v3acVji){a^sf|=(mlUlemjH zgN25(DfC-HKli3csELAj<@rYl2VJ_>3ZB0R6&2P!VUNvO0jdThu{f>G47Y+;2Sg({y-*llSPz*=jZ|2q@2iZp#-(b2N*kKI> zU!6!DS|X&Xk+ujrlQDdv+wv$&7zVO+vbg@T&PrS2@l=~l&?&9pj?h?0X)Bor(i!{Vz`b;GFUkFVw?Pi7>V{(|Sk9VR&K$GLAmROz}KS6i&sP_PAeDgHybyX<6jAK%qSQ4lVZtY&HMQ@ z;JmScKX&6`*Yxb4reJT4w5T`yBW@A0a{AW4jeq3t@9lW~^WgN|bOY$Sb{YdB%M79n zs=x-=S7KiN`#5U0Hf8?!9iZI#qvn*k!sDJw*ku6sYLM`^Ad1Ocv%@z9Vs9_`oO9}i zv5u++jtwJc;>deS%BKmb=)aW<*(fQjVd4!XNW^P`e78Tu@WvmP9dSDY+lHjJa5{~T z&I)$K{#Tk~gOH*=J1e#5LA4MLeL%e>Hw^5W-XU$y3fHS3$nQ_fD#N3L0=pTXH;}7O zf|&}G!ww94UV2AUf|{un+VYd>W;EXVorc{}fUZyu0^KcP@;=|K8|LSm!fi0?pmLdN zbx;_6fc2%KKpqC_An%Gr@zgMhMTjeJ$7oxr~RG{kkuGLd)~p*o#{=1eNEwa09gl3SL>}eY}A39_Tl^#I@r10 zpHuHwr5U)tDg0?#nP~C(5yoCoUhnYJY-0E^^%qhDf&+=e%Yi(#xpOq7KQrUN>gjBH zqWO)xE;uj+9Hle=O^)6GDCBA(V5|eDEjukowi376DSFRG1dM!}&W>I&AzVdy%|2=PY7N4`Ng zOvAsLdIX3`V|XFWbz!xOw-lLLb$C`~2Uy$3x+ESo_-slruv-X!m-!vbLw{OMsvaWj z)qKS`A8!d2Nx7IFpA+UU2iryF1^llpi6MOsD% zJE{(|g+kd@f<9JzL<+|u=(lC_#hOS;jH~t&2oz-BtaO00K?pcc8%~IXy_HW_aF&9L6Sr@9)vE-9zd{ct zK=a(?mB1PPMnz}qY4ScBJkRmk!RYCP{vRLWn5hHxAf; ztytFUXoMue$tC=C^DxOfGY%{$d;2kE2Pzw+Qiqn=gn*-kzYa5?V95A>`UM_U%Ns)U z@vox&ye<8~^N7Q70z2JkNbpYAB@CU^h|jI$BbH;HVSfTVSN1J#hv9cg1%}Pl z2Xb^X(#>lE>3xQ7Y@5TyIs{!A<-rmas(!xy!v_U7!sBn2g>0o_P(1x31P!+jy7+tx z*-VNF|F#Vjw^>Gd(QQqMIW^pw{l6Qgg#x^XniGI*5JqwNpr>k>5`~q>02nEh+f;P8 z-Wf%G0>7_mJbHV)BRK#L9S*v)r3p8xuF*DmcwnkeFrB{FbwMxDUI_o7$=aAqNch7s}g;rGQw^ah8KG&FfzJ@v;_6qvjzADxIBvx`T2Jf)W?LqwWj{5qOV1{>P!SZLO zEP<*63XR?gw|{}(G|Hz3h@5;nLRsOCK-JnWlXC{6gtwo8b)dB6otehUCy;VrdxdaU zXqI=7Exg@#%ydH2j@`<2GIZM+Dy?~yE9|z&OzS;Gpp^heK)An`hM>~oXFYvmG+0t$ zi-pRotlVD`5N+Fe5{I_ys=*1j6BuoqDK8bCiOOi*;jaYKwotiH8qJQSdl76$Y#Ikr zaQ`68h$U4x-M5)uzOgu{gqo@f!-bGVv(sl^G*UsEl;tfeyK!zq+1gLbauu+*ct4R{ zw9{~7(!4x=`$2XX=PF|+0S@ zzb32;IW@_b-U9>v5LGb>+US+XTmNONil>A7t?e)9GWi(SACWR>*+)UaOY`E8%~ZI{z#lgpSdV9(A+2ogBpDxGA%m#zhw^{L5tImN=Dha0h3bQ zbPD0&nn6ZDNE4x|RulA=W(r489ptQDXe!|!wqc(3hG!T@gROSQp+~OdK7~T;s2EG$#Sy|mk;o;K-Ct7WDaqDU@^eOZC{>((oiGw zV}~;d(5{N{KF36oRjM{HVcUh3?&=r4L%{^NmL4vL7|%e%Y;Wc#gh0PH**}O1rTk7a zu>VB|)}SUB71mKJ1ASCxs)#H5j%;fVP3cu4E4XV(SZve6_A8yX{6D1W!`9r8IjuH+oHHz44p^q2C&|-YDQEu^W~6c zJz%CAekFgz=?aS?K6bEg$4V2^gk%idJBob=BGZD6Pdn=a`Ec6~KNYV_kW9RtM~!{) z>-xfPwHJmz9Zq=LuFc_B{!2`I_r|QW8BM?PbE9EdY31UmT96dn!?p|aT;jC75`I5u zjkLois1Wb3@Xn2mlspZ+y>D~}7`UrnY};fyfN7jRKtaHrX_2k={w8Ji82k{gI>e5m$2i+w*hjKDB2{5M7YH1N`262RR|li}ohgo>7T z3oF5gN&~p%%zoZ+|K3<+8ebw0C1bO{b3_I#7wSxNGlbL7b<>X&(gD=BuhdirLv9R~ zJUljFuFLgiG1#yLaP=&w0Vb|GKp%{V`R5WCWF7-7TsbwqaReRks9k>vl@kD8HH}W# z!Uee!Gc-$pd+W{{bM;Blk81i#c3P@*E`L+AC>=$GS?d5ZS8?K_;~g41s(E?gk6V9+ zqPf~08-J~FrNJ^6u_!kKg1l?YSa@GPe`WnYC8%hEfxj3a-?H)yL7mP@;S`2RLqT7V z+74#VRs}nRR$&bsQ?oKg83t;cxcq!|fS4Qc<@eB98dpJLLo11}w6ZuKcs0f*`ud^Z z<+hreok%}8gxuhp2tdEJre#b~bA}GzVE={z`VFeZqgmo=a)}x4&PLapsZHTBE@-Ns z=?&n%F=TC?4_*=D7OI3zM;|gy^Jej&r2p9r++er2q&L@-y}c#_r15P(`UAa@uzaKUNDG zru@eab#K>F$ag@qV9QyTO0k0pH+uM~y5(?Lc_4j-RnqW7?^&?nj89%{c~D%M{)CwY zUG`_LmP*GPTp|4*2Db%n<&qsFv;%eH>R}}&Omxe)?c16{FLR!RF1z3ZLd5PU5$N*ZJ!d=y-MMY@-%$UG_XD=5|CxVKg2449j5WmN`OyI@@p@JPTCb zyZ=YnnI_9}t3Y@|j<7)U+yBB|L(+YqRLFlFSFY1FXh@PT7xJS*GR1@%Oe;<2k+TKq z0$0PTF_EygdzP?-czOkd_$|@o`a>POYUGCX;T~QbaCYa+YCh=F(1V4SI>l9aq20D^ zB4_yMk*qe;eU0X{VCOu-tA=DXnd?p=AMu(^aWMY#1&ATUQy*9mOEsur#sFKl){FC1 zBH-kTfwD7e`O*KM_8nV41B-Wpt6ThReFT3|{Z^v=jfCi1Cf^k@hSM|rjTJ&A=ip!? zSB9X$S>br#oE6wLzezvY=?ChPw8q7|UyTqcG7|j_g!ntt<}^9_Y^-G=e|Z{Px6jLw zx1#finr}UtX7cr|CF}Vy6wGLo!=uyhC1J zoy*OiSWCDX|C5IyS~3q$-v>D%{l*yOXDx)y0Th~QC*Rlinzc34Kzb~=CUeQP*#Mw!atvf@Bcs)8@4&PPG&QoX1#?fAxo4>55EdJOnr+n zQ`udeO+lfUUz~2e6osf*`@1g;PFQ>ErTV;o;`H+Np$YIpC?Y=$3qUq#Au0^S>6%{J z;flH=q)aosNo@9ZByb&uBd(U}?#;Qgqvh4-(9M^G0JVUGkEdgwyIa~tDt~O#GGrKs zWa*wBPSkcNnA8w`iNicuD7&F3IOGDP=YLgRTg6+qPBUM!p1>>g9DWANs`iok)B7ASH>9GQCJ=}iIeZ;se$EN$Qm0rX znOfs&Jn{_+zymUkh$G3}iGgO-Ov{k{TESfB)$*yLZWT7?>tRNl%pZ-h6GVlTFH%-d z)l5@AuOcf=ff?-&oVBqb3Bt7?RmcK~>aX2Sj4Qpg= zycI}AaUK0Vf?0ow0Wa;$qC!dWE^LO3olE<5W|ejl?d=H1R>LWjv?~M_7WKUhugy(x zbGH{g9)`uLFA1!-;hOVEus-6Xg#owKUKxHNdP;m%`p9`LNF1aUGzhcqjvT2!zF+wE zT@w>xZkZ`sRs)H_81YKtTyVjxexbxLwx*v`vwzjh$Gg%nB%*JaLTuxk@bQMug2zkB z-*D-`hnQdHLd)s@)lz#}nABHjp`WLLLTx@PobA^*U=;jJ&aTtC#=YxiE=zi)+^5Xb zP?KRNtg>$H?S&eG2n`NchY7|)9sAN`F2n>F<=nuHZW3xY@WE1*<+COA-xGYidzA~# zrsDt_w*ZiHiBh(&24JawG(fs1Y`m-AvQyxK;NYE~kaIOi;{|2Y5o1HsH}*^@xzdLF zmh(YKL7gL92D(bgOT?fhiOo1{PPd(VdTLhOB3Q{;E8rEU1*Y)@^I0t@pafMF;9Myi zp67DK9Ev?XP-d1YxV)c9h&Wl%eg@?(2)C`5%hJVQYjMacTcG8J*5`IIBXuI$D_l}V zxLMcDt_`LEE#FbMilvE+3yq!83BJ8Ud_lMkmX{;UPuu==yXZ)V%kT?S?`IYvd`R%rzsQ7=oFyU3;2b-ayab#dP+j9 z$byC&T`Wh#De%6*leu8u6~*~24O&b@?b&s^Al}k=>3I>yK4kDsVF>mvUbcNkOqi5b z!;WqdHZX45gruKuF#7)_)!E=nJ-| zN#U`noTB*Ex4q-~c3Ov6mW5oj;M;sQn^AwUHZEwjn^T7(rfPNKg$(wEV6>JS&XmHg zPEqzp`voq8>|Oso+dReD7V5K0Razdof?d>DstpJ=%q$H3t@n#n#Z>yi;idqqT;{t= z?T1MTW59Ij{j!CVCa0!xZ;tM?RIR0eW;wQ1mM}++!Xv=D@)~)I3WTVc)dOr-g~@y6 zKtjz=Q{!|XVy}v#(0o7DfHIC+VE7au*v?58&_n@0CS)Nl#Gz?V*8BB@@T7S#{`&!g zq1L=fSDA!zya_oS%aYJi(OlhPXh;lYgk>0pnL~NxIq2!F`jGim4G_h*fOFDCp%RB5 zX`S{pk1g&~E9Jhkbb*>1WDAzP;sDK{4#i9_X=u}?6|RV4P{{~pFk$M{ySzrcnxWhI62v9mEroRZGtpArmb+(W zkJCdd$G0AqG9%}9zIDM?RA_^yx*+2YT9*RK=YgoK-FEnuSei;&fh@rLX>p+s|3Gj= zqO^SwNWliatpAidM3^+fh&2us8Jd2|0Yf)bQ0R$XfJOz6kIC%vY?5auHwCShp%oS zuX&?3vVEHNs)fIE{SGb3^1HuXd|X=?xzGI5bsmh7Zttsu3xI}rnwtsmt*kH-Ij@|U z{I&hJjA!`u8o-X_1yeQtsrtEn!Dg9t*GrjCPY!{bOabh$0BMkhw@f!x0M-l}nt^!N z@dW;8#Z#AF;&3-$yBzO(NsX-5vgHm>9FlHX?UHk3U3j&Hw`>6CKJo@KMZBuU9e#@Iu!x3<~A$06nN0io0#NGOtei|G#&vwxQt3Hh3 zT@g#=UFlf690>}*Sz&E~#@i@kG)jTp{9@rTT0B}!Eow!%X)-nS@G;Es3p>v)*dP-1 z@V!Iitz2&)tA{|SSsOQ}o?GMpN*f-te@aP8+l! z?=E}BY4mBx1pm`44lSGRm4K#AilJd}LEGKl?Duqjhzh-)w%dhRv``5>!|IJb5<)oE zHVS59X5+8nl9>HTc_W&}SL6<--?DrxU&>pm@^bH~tcl+UkM{~AHx|PjaH(6as<=aU zS2yy}O3$L$4XiFzK<_r%D*BeO^SzH615hd{?x4L3oKb6C%Eb)vC|WN!GSVuo%fY8k z+yD9v`iANo3}eS$L9{jP>hp%y4cRvo6Q2{CygNEQwQz%Q-1d#%&$hk}kUw?5<=RHw z4Oxm$pTDtX+X@4*Jk56TYM3XvK5owjsio!G2Vn=yK~3MP`o^_%yi^i3X2@^BHdNnU zIlrPI_-gZcLmTAXO1GHh{xxm-*N#xO-f1{Hsj-2Ps^MQd(09X*h|hzlG9Gt$1z_9_ z%^ruw(*XqS6z&dOm?9~?5z(#tkh`B&HyIyhUoeUV>tU_FHF^`A-f5JkOaR!R!~n@n zR*Xz$a+PF77K$$o9J|4LeH3qs5eCWc6*pAiHqw4K%vS2W35^gNj$P^YdQE5VIQoX3 z)eUsRXge>kOHWCClC?GryHsv%d6FzIVpZyONKAEZ@75t&H(`GW79bJ?(13` z$}qjUhoAXB$dzNI0HnT~60v4v)jq)Og%%U=uyFp=D!tSgXPWzMmC6gcxXc zr5kDH-hF_%o3&%dnF%3kJod%CA|0W6ZeHQG40uZ4_^>!$%Ae;wHrwpHi*Ne>Hs>IP z8?t^K>dAXHbU%ZJ2LIgy)!Wv1J#-V)q#9!Iwu-HK}hXK%T!)2-C#FV(UM*?nxXB=S}yXI=RJ@I zJ6CS{Ysjt*O&%L^uxQ{H57LIRUcx~QdT?;r94zTRtzkV}5-y0IKMd6_6+G_?czf1= zE`IvDt(MxH{;?-&+kIZLfTfLF=d$ouf)caWJE{k0b!uVA`1Knmjfx6N(~mu_oD(k# zul!gi_PB#rzo)gWz3qI^sVcrdFO_YWd}qEvBMo8!FEq@#aQQN=_eeF+swo8eZ5VuO zNo~5?gK4$wa2SC8JA9$yNI32Yn4=jESPO@xe2Y!$n~_^H!}0|III=>IBM%$DF`PdZ z-WFL34XkE!$U#k6kSr8t|kHwG6RBk8_`Y?guiq5tqu38PQh{ z<6z<(J8KnYWE$1xqlPx()FL`^cP1P<_C7!zhsT1uIY}HEZ5HIJ#ZG`@ zL%^yUdrIgm1$evKZ+ViekcZ0dq0a*KH;dg6RpQ!L?# zYPgdySB>131tuwLUwF8a8%6zci)S+#&DpDFF_Uk-MQ&QeBNsP!&O*pWswpQ@WZR;T zy)FNWs@}`dx$=zq5QeHYtB`*Vh+30P+P_Z+n1NI4^3NM=-go97s&q9rsm?%zegTc3JtPsWA`0j=LuPS)lQb zk=kQew;^!ehS`M;oOxj+bpy(F|9Yy*%Ubq+S=ZH1DW9G9BhO-lUTT-toreCoO2dCg z@cH(YR)awY0KS&O$c8EVrME&Laa~VEpS9UfM{L=Rf3<9hTGr?nka#uW{aKLj zV0hHw?+TP%w70yOFEx-?i$OKaM-_4lrEv&yI7MnrSiWjKgrP&jD(K0e68}<=rnnr9 zUZR@RN2|gt@cBv#*l9Ir-)vi!%-z3Lq@r%LGEGmmVFpl}VLC$Q8u^B+qouqSo!DD= zL*VeG`F0l4CM}&8`tWZ@HF$G={H>y{$~~bGmOQifzNLAi$5`L~-u=V?EqAR8{;1El zFV(bMFCV`kzqMXCwtv!=yT&&Hx2Uqvox|?~kRi_*f_oj6IQDZbhzxb*rlz9hcxhV* zuNklms^-2-wkR7c-Nl7bCeGce8wCEZ*nR%qP-L&|gn*+#wD3KKxevK(i}+$TAY&!? zQrs-vVg=~-hjueDvVRsGeMuw9zK{*B(e^wxYZh2~$E${@VKs83S>a9VL1TF~#8lH_ zXS_^1;%*-k(Nsj&PzH|w{D9)^E%!|VL#O4}6>iCZu=*Mzq3d$zN0sn1XdZi{r@NMr z@oc&!PMUiQ z%9l1AnnPz9)>6G+;rEKk)CP6`3?bw0k#8U93;7x0umEC1%jMBRKj@OQHUG^^KsNL~ zE?7Roxn1RmziUnurM)NN;GDp|ECHo|S2O}Ir#LtzVBlO6ruvDkOPzU9tD7yMgc%JK z-jEl2a1smRduKR#L(d(#fmPEzP(5e=+DyO>$IYT`P;}<7DF=x+O5~3>K)S!i@romM zEtCKr-#@w*w9}#bH}#8fNQFpk*+d1d#6Q+}ALDkof-j$D*w7$@t*>&Ktz^Mh#8meS z8!Wzirw!p<_Za(Owx4C8dX$scWt*@p?kG&Hfb)i<%j5mgqc!j~ekQjpwo6x5RlT9< zLBhXzka&0AATFKSx>MZMZ~9c^EwiW5*!+uMpc2+%bNKf9+EdxEe8(N8s=J@P*GlDf z*lvq;D!GySDBbeYrOzPDtnl7|%{%9KHD54dm}!P(0~m*Zs5pCTEZx&5!_2ZR{ia>2 z;nq)8!ZuMAUXGDve6rM$cN9*LF#Ae~ca{#)!BVd+C{*zJhHmYH{nD!1v}X`2%PkkKXdwDqkow6*T)os>!>PHqgkOhr0WLYLPt&Ibk`;d4gsZ>a_EFd^ z&~hHANig?@$gy$iVw%t+0jcg0LW^0M(^h1o35lP>Kwx=mo)`%@GW?`oIVX!`yZA&@ zUq*?|%ajDBLkwAd+U*J)TR|-$fF0J%&)lj~qLSg4eS+*;#?0@JouiojtVj@ls}B@9 zyOf8%;%cySBf-sV{U8hcuyH&5I8$fRuE?S@l*$67`S%;4Y~3G!Ps}k|4XM=d>i`1b zXAhsY5$JB#|AhwOz5Io0BGnyG^Fkc4b?eh1_>t|k28Anu`mtZ!k#e>Ms=az1{UkP` zU3z)}cB1y@=AO*S`%1N43I|pmK=Dr(3a_X@@8<0aYwPrT2r`rd*IU3GsFh6qI8OjN zY|0xks)j$QMYV<7vXH=)AC=XHq76LVf8W6Gww}9`Aw^4zpg$=S)ZO62eQsJClAqbP zHhc!6KZ!>k`Q4YL==owCLFbJ%h$jY-5WcSQer_b1O-2UaDT-@mex7&%^OV62+_5Y5H1F`_h^>{wf1G`HouL4fme!oNu#d$ zxyVCAKf^ZKUnyHHbvLJaZ=Zg0fP+_gZ0~U(vUn-6$NxSkzCYtj3eB<1dl1B~(l5SoxPKVbEv1{z#k^?v|5ah0fWb%i}w8&IlQ5f(q82AlT%7y&E#*FYbw@H2cQp(>xKGeswA{S;C%UxSf zpdl*&v8gNEe>>eYyK$81d;79gr@s~O=+^a$_>*nER-(g(3!sKI^XP0fBvyz*{;wNQ z>BbccgYEO!{=UPcyL*}z3aAsdvhk8&HOPnkeCq*9X<-?FraM-iS%va7GONO$*bxa| zFFm7{AIKwYi$LM+`zErtflt)uN=!nyT4+&z3Od^2q<~F71F5u2A%XX78pD~Q`VP*| z$of_dUwY@S*bLV*(AUg^0&Z&jasn~Yc; zg+0uV3+|Q>tkhmmJmc6#Ay7>LY%ijpIL+&^|6RVc_(VWTmz@?qeX*4mbd-xmp;kNT zFX+>MI{-2?Q8S+xvJ9Rui2$~%dDAIDP-|E{EZ7V9j?h?l1`t%l=!9p5dScB%liHi5 zM!>M^g-RO$+Xsod4$G$TFBFWp!IMN!;j|$Bnd7;0LqVza!fF!;i%uv0BZX8;X;K(k zB;~e_aKM5Wx4woP%e2Y>ZFAUL>~`qF&s}eUZSJ$R@NWu?xKX7&8l&OKg4-DNnDlyIbT^JZ%YhSEEcc7WckZ09Pb%-%lNzK})vBM9;0eNJ$PR)?Cz`W%jI~3a8Q!E9me5W>$ z?zfywuq@1Zu~ajH7TG7yP6mZHpcRiVv7#G-ySHaDthu!%rYP1gxF7)7|4OK~l~Sz= z1Z>@m)bOX`Fyt4)XRo7l4Qu$U|0>v`jc&~CirzflJ^ZP#N4>S*%W^$Xwpj7Dz(lv{ z;!i7pigSxsLr?U@L9YHZ2p2Q_!i9%eFYRk)02sb9$EM!$x*Rwd1X=QBdJ1b_OQN6C6CcR7;I5oQ3Wdf(z3yD4$<z!&xxwBgNY57wexBp1DmyH5C_dV5M zG9+A`Xx`|kB3WrYZulPv5^vALE=f1|m@GJqMDdVvns!%%gC-GrcOyf-RZq<8Osd^) z=)w1gKn8swR2{jUua52$inmlSG~CxzBE!mStg`QkVUQQJrk(}~$VNyL8p87fRJENd zpcWUzec|s0cnef-bxI+7X}Ci754_upQgY0e><=|IsruUi;KD59*7h;MouqI@ye+lm zCVP1`tFA15TI+uSu*oAgCo(l(Xl6*bwWPe9p}{SPfe3filGIARL`cC|zW5oI2xz!G zSDwou)^`Occs+!HUI+lYrxU7L7k3SF3?kK9ididv5x@fZ{+qss*r_r7XZm>l2DkYaa)%KkMNcwS#69w#6V?JJ=J^ZjiK>|JV|~M3d$EK z7=EMGqO-FrDWuN?h%j(qKl|Q~C~!=E6n7b}U9n}nb*c+>y&NYrsJeaf#X})w@1?lB*H7!=;&?fvM{82<1xXGp$UO*Bp+(UdRD{+))fkSC1daU`RJgfu+7$v2lE*S zPDjh@k)+;r^^!KfX*OSONsG^_M}veg!;jP@N#Dd4ewH|kaor9k~MzQ{HEG;KMo}L*)Q8wVzBNbLT)~oQ~)-S(0`vRmzS-I7I!%<v?|RLa$cVO!y8#dJv=_VFZ~8#B80&8 z8U9vZhCZ;9)}FV$tX@^n)e_>{V^i3FJrtn|F?UhV0dyU65i$F>#9+G5saf{HjLynn zQK@X6{_^_7`oe&t78~nF5KlK(3Ql_UBxx$01TJy_Y?AoHV zpBC#11-I5~dK+L{w7u7)B2Z0D({DZu`uQ-rU+60Exiv_DZ~CqNss4Jk{JKZpJ{$`M z2%Lm!VQPs36AlxZntcJKnttp5!hS9s>Iew85^#l9Qnw)5G4zGp;erjBXsHY*HQHuJ z@~|9Nli{BB^YLXi5{IPg~U60lq4VBZriyD3vlv--- z(#6a;#!=wIRLHg5a}Jw^C+h*Qgoz7sZ4=S;_&e(t=3RZ=u|lT}$J7`5WLLPEe^H>* zwoz|!kVa!KVdiV-VBLJtLM@?|XA2`$eVchmro|FBeSqP{po6uqS8bSS9SgfGZ zhK_-L-VdE!nZjR)X6Gz-vi$r^k3SC$@;k?P@gD-h7l12N+ST(~Q2X}S@$|>r3eRmV ze4&MWH#H?bbn~>l)y8#mB42vEq2MZ%S0IM6V|Bl_z5H0A%~D{SJ>9b%rP%5dW@m*s zD}TWoHA1T~P4l_JAnQ{t7t%j&$M9CU3SBm4svpVdWI4-Wi5&3C`NPXP_eCNw^zexT zr?5xryajkgTOGNDE9&~w!?HlhesqLOu3tWSqGIx&71N9Q6?|NxyuDdXeh5$TOBgg- z`7_Q}MzIL;$_!8MsL*JoK>N~l)xMlY_MG=wLB-`Vat4ER8Whb2_NxL=3IfY|d&_?D zeasXtXDGsPNq3N~B02}cuR|_G=@+DRL2xqnuz~x`HJ7v;JHIyotN|pvA>Aj z(OU>Bpa~HK`;r=@C7cybzVPGH3(udy5aAjQKY%6d+H*0u!ew}4h?vn|glgWpNt zFbZ#&st&XYPS4WP<&@axZpihl_JQEZs@TNQ7!X#nGHZ_wFi zM43vN!LQa-re&IRDMxo$Erx$Bo28;vPP&Hez{cTXXCB&lQedqwhFwI16<69jUZ)4x z+_VPZ!FEk`+RuLAvQ1*duLVqMGa_zZnZ7Hd)IAig2Gr>umuMP>e={6R8kE{O`Mm)b z8#yLl{%pl&lULsmeUsQJxPKZHT4``fewXIaX;*-MY+&K0&&ELeQRw-OY=a4UuO(uyCGdb2;|P(R`33>^#2gxc0#+nE@x zD$}=Bw69K^Bfg~fM@6Gm!|y^!^kwo=^c7?E@mCy2(!ldLqJ?Dn?z~gJL8%>-lEBfj zMH;UaZi^KNZvmnP(3P@;`&0LU%{x4omUI_7geDb07_VErv~kY zobTy{v1W>`7a^VpHK~fpsf)tpu(73nIpH9U5%6p)>rkg&3M8q{~8BDIwWz z-^lauf2a>xgP(i&V+VA@wOF}G+oQ)|T<@lq7F=22=p z+>DJqCS8m|no0a%z&99dxy(zbWkq8k)v!paWXlqC_1Y^xOlDuIO<0BUsTGw+XIcXDqnLLIM9^5Zr?ZA zUGzoa*6@1F9IXx0fl6@R;nm=*jV(rpkC&i1g-;GrZPW{&5n~#IsWuZo|3R=->+oxR zOCCBsJ1|=Rs}jCr0Q!$l7>1$aqo+(nwAAY1!UyNh*6UAideNfo2}X!h10Nwm4PWpN z9nJH96LiOpwxlpbH7tCYc;RNkhy5y1O^qrNMw0IrXdvNAI>q5=K`&YCl59ps@a9cE zeZSJB6ZgNW(NZuRoU=(q6T3J0v|+)lS)*N1hjLRENecrlUvr++_KjI9w6`|E5yay2 z9ycyRiq>btqG;UPo&q}2@Kn%S=kJK>w6UvyoUySeHHG)eGR$_~Gu)I7sMa7~#Su|D zZ8RnIZD>f8AAwe&nG*AK4bVD3=wNBXfqkRX|yZ*;!?jTa7k1o&rH5)fqVL*Q- zs*gmS4Fc`BBohll12mWsKA{OMMJA)SwZB;-SMxf4gN2qU%^0Qc7GJ}V+(3fEq~~TU z-I8j3%HPw^-sB>#chonQuX;CNZ9A1qH*2$;wdEzqW~79kuKs|H>Z7L?u8Ab7P^m3v z2PP<&)gLJkU$(-&xjc6qV2o+MrMspzb?jU)m2;B@gL(K<12};%M?6G|X_Wom{*T4^ z$1CPf4^La>@ea=fB5hoW=4nUQuGk&!?4~rYuqg3D(XTG9a5lr+<|$b6nnJS+!ej$9 zAp~*KlfWXcoc3Ub)nP-~pXGvJ`?Ar+$LIc7-Ldd0mtHjy7&Vv5e~TO0ASFvPXWio} z2rsmKwr!!BoQ1p>oJ89QqZVkxIRmt5*V0mpO?ZMh4DFp;#O1j+ZzSvh+>V(lsRvFs_6@5>-RLb$?F2a)y!(T?^xMS>k$& zU@GV?55@2f!8Wjk-hv5q_fo>IGb`N8>u~}!*FJb}m+8nc3+5w!mUBe3)nGUdmxRdD zZdx8$D4E3mxRD!++QslE<$a%lPzT#K^p`)=sT!&~ z+b@J2fo6gVN%Vd>M5u+hxmB=3!KL?{QshdjX?FSzSBJ^AQOl3CUw3W|H-%9@+~ZM8B`To;eseQw@lVX z?4fw;mfxXubKTbTCl)p8pi&*40bpS@{}y!XsD#>``b@fxrU#IL-iY@ zMrPZE$-(=HJ1n<>Z}Qv+^0dqm-v!Z|<$u&fLg`xahvzn|l@Ckg)TaK8dW#zvL>GFA#ZY8Oc$s0h zonu~(4ZO7S6+E-OAyTuA|8Td7htD71yDGRGI@M^LPz7x+-9CY(8{jslk>XbZjN*mr z4ot#+omZxASpd!qWAt1klk^)^OrS(s+xEw=4)IoO9&a`3Un6Q@ICNNUSBf&$`BfXa zVzX`U)?8RF?=zY6Z^e<8znW46HAIQwFZJUP`Oz+2V9dthjlXdn)U?MsQod10wTA62 z&SekmxnuP1;GI(zr=X*b?=&3`&TLwFj2qAps5in8|_M-bZUnc4U?@ZnK zp{Np`J%#yf%wMX-QPI$pK9pU;aR{#4(DHm3>coPf%KDEy@zK9tzF! zeHs|-rYu#|`bHS1ug%(Y>5Z{McZjg+03_@*e?>M~Xusv+YrB2IuF&|lk4HQM4PTq; zNHVLvU69by*T0@Ef!$Qz3$Ise!*X2d-VxYW`L+Beg*1&_6iIr8!!&pa)BmmYObRw% zjDH|-(bIkTP&pNU>`$mbN-Ey}ykyL?5 zeqj42n}z915aoD$-$qfUBq3L9!&SU;v`R9YOEv7&$>)PFDMg;q4nR zxiE`9;YkGvFcexl`bMr$3M(P?i6hC^@0tg|36!24rJI4QjP4Jequ9%m&W1R^Xca18 zum%{prDXnS`H^Jy`!LR2QK=jq1$f2Cni-%c1o@htk6mO_ z?G*I~w4o9&4roW=fBa(Qy~a1lP#f2|8eG(n0XYncE4#|$euyfMU7GAV>2C~(U~hRW zSin>@i$BS%mg6;?M#GwP!yCpxcImepSde|rVa*%hCdBNF=LaCV5%SD)X{j%BzeVlIiUU10dYW zsoP(XjPOIWFc4rCKaD-j;{>bO^c$XmB(qZT7+q!mbcDh!KXM&*^`7Lrg-;LSZU}~Z zT965yjXV58*mE`T_oacEmYEEf+v|+_r@&;1#tmPrhK{*`(0B8-)^Y=7dU;mPGte0EK2&{) zdNiD_cfA-sB<7_)4;=%91x2GC(=Sl_``Kncz*d-fG_9ViwYQbLzBORm4ISHj%uZm* z@!a7D!p~h=j`}57Vd7zRz_u&hF8`jy2?Clg+z<39QVLGdy)MwX)448;&gGIZMra%uUNjhnRtVOx<2*fekx+D&8|j@>hax7+ z8L&#>&jhe#X4*zk=z;Prr*B3ds`$pQ;b9ZN7FpA={2hhjTXx=GRQ1TB#+ecBjKwA1 zTBya%DM=Y&S?UN5S!w(Wp-pl<3IwCCGjRI}wUug%#^8qCMYq0@tUxXAO@k;(UZNbJ z;nvU_QC~6|6>Q_Jj{_Fo*7;T`Tb2vOVWh54QVvNB;uS)=*0SBOn%8PH$4yh+hx5LT z0es7=@d9Cx>U518dDVOCOwz6p2Bn+I5KA{8_U?V_t!zLnQiy{eh&Zb)&B7Idd8xu! zC3DLCzJ4cReMZv@%wqsVctJuHRl{|@4@VY|7G~qyKGtYi-8V1{OFRpHopDkE zM5_pHrUB7+iMR8G6RhM)v1o?BvL0*TlmE%qJQo7!hpD}*U6wP#F+{j>e~$ukuNGa7 zQf~X+OYY%VJW-{lG-;dk6uzELfdN0SLZx16x%vSqj-bRx2YlEX`9Y!pFtoya290;D zl&w-U2MeE4_$DComYtml2<08YuzG=TEGRG|b@OUlhZ53)%s&?Yi0SX!w5J)YN!%)`pWYG+j9ryjR!um%_5WlWVA;`&pe6 z*Tl9?z_9TTj@?5~?*IaiaG(YGHdvY4O9q^78U54U7C*EvR_}^l^v_|~Pb7v`*!2>5 zEqxEt9~(Ag+u;2xT@55r7~5O+nE3(2ZUCf>XN$&yExnZRw;E&N?HwN9pk8I0xG@WO z+4b3n`8GgS!wIv3$$c>t&jATCfSI*wRC-8nT9a z`hlwFel~dTbT56~)F2#CgVTTuR9-1TA`s|v@NW!3`X4PtJ0RYchnH6;yt=zn{zbGO zoy$K102fM16;O_tLgU$=6sEy&_qcR})@}4xKch{Ub=Sq6_6giiSq1-g`DdT$m-*tS$Fht8adbGyz>GdB#hjUqaLNyW7c_XjV_w zXgRu7jn)dIkmdW@iKk8^G^~U<2C{CQ*(<}^1g63edEG=XkGYm4p&42*@W<{6pYG~! z-RwU;A=er1q3vq3Bqpg}TWzWNb$!C4JN&Ep+-4T^{3K03e~`i*$_>cKQL zF6Cz3Dy0;Kf_Z261RA%u^pet%YJh;@ZAcTcZVboT=US#G?iN0WYAK5Ia(_TsqkuJ< zkaeY@Ebp->(^`NijKpzh?0lxTG(8Te$yw1_|8z|k&DK%EI)#6ZUZbA&eO2wqkV(n! zI%h>Azh|1br@oO zhk^YU|C5IHbsO1y^=39G|G9rewN>?@xA-*XnP0*$7*hq^#Y?WUzmU!tJp~4YF9?cm z06rWMP7*U%rTO1aO&vnZ%G&RW;t){V_vgL|=WdYnju2_{^F?;|gnxJYTFg`K?pRr> zfDq_!pdFucHVXid>Sp(n{DiJMT3T-}NQNC+Pz@s?)3rDJ6KhD|e%OX*he4_&tQ*q8 zCTrJFSYJA*Q6avzKh-jv5typziV~H69aGy#w4*u9rNf+c z$&p8!c13%Rpx$XnhB7X0&Wb(k@+RI1Id|n2z@q3~buRhS-~>c>$=U2YLIR1Q9pc_k zJwaw5)!E*nwY1OBYCrx1hN@c%-$eOh0g?XU57486Iy;_jAZvPQVIu?=FNN9W2(z5n zhBnX0n!}c3)o?e>iJ9#VSX-)eL&Ba6d3?zWP`=?ie5>TC=TZnoc~^m4uZCBDa_1JDfYLpYw0 zorY914(-cxj^A{YJ41#ZHp6$0RI?e}5VPQ20~PWnb5&&f6_3RWf+pNUTZi zQnT<5iK}1@zpJn?;dFSf6cG6L^L|9_RW()7K=uFc2AM*S>yhwWsRE^$FMRTr)2L zSa1?WB?gd&fUiDIkMcV;g2d~za~YxTl1VA95-AvlCy9(R4beIDxAp~daEM5m3p-z` zc`IHW78_r~z=b1S?(tBiNq^qx;f}&o*LGgKttj4p?x^3&Y>TjJml~`E@P%&*B;DO| ze)7_k4(WdnZw&wqyWJRry_lVW!zHa9X{2ZtE9DS-8_R^;D=AfH`A4JeWMS#?*Y$(- z_l((Uq!R`Q6Xx9LnhxmX7eBhr*c?O(BQw0$Lw~`U zJdtfp;r``;Yd3Z-JOz~k4_No7`2~?znn=%`CoEtZ!nFJlG^@=VFr)G=(sg`sX2HA5 z^%g%Pb|<-^!B)56;MHwf;?d~eOiF)Sz++Gs(MvTzHx^A8ik-V$aLDVG%CCu7J4a z|5O!vspOe0+E&~8GZ1Y7(J+FzTppGIIiQIT*Xst${JS&Xq^&PofOhyip!#ZE=vac4 zZlj2R_`<=!yTY#G3GO6oFbHM!1q*LXvMn#>0o`tS+}8rDK@o%7t;;!$}`y%#*OgN>KV1GTjWS~3M5@X0jCdN9%v1v z(`5y1>)V_fMiP<*9{F~_WV4Fc-i^10ZF^I>ND+JL2@3&tYuM|-d6!`|*T(GOF9cR~ z(1ll8`j@`s9|UCH5fZbG9{6t%%LsognBM$M{Zrn8+{NKlq6KkR)y4Om)Bf$S@y4Px zjOsz@9)$m=?WIb%48IHzn3gG%@-BCnZgyCO;MiR=uR*Y$Bz2D#<^>46yi>e0Ya@vw z+`Yql!LeIgzbu&@a^Lp#k_F$cIF8Tbhbp#BiQImMhRhpte-F0pMvwgn3oSCzc&9rT z3oYr^mijl<8i+SUHS?LMi%MBI@)(KNTYl*o{@YL!_{?YFVXSkXb@YRQD@H54s?+d6 zFVs`WDEZm9BwlGjqhCGCWrCapE30msPng}gKl z8}#R7%0@vC`E_gADVy^%VZ)hFkK%41xG?3`UC_5BZiH_VG~S5JIeNP)1dp&ZS$0|( zukr3?^^Xqh1+h1_+nmhkru0JJy0)P8(#V?N6_Nz!WD7C^V7z;!dpiYx>w6lQo=YX) z&v)G~6yJ(9n5#EpEelZd^TxCNiGaDKxxyWth|@SB1!a2Irwv~~BL>x9PPW*FClQ?!z{M6;f% zeEoO>#Jexs1dRf!YLD>q#4c7*qT@Ce1h&)}?mBv?cgvv_vMbyVNXEACw7M=EUKM@i zP4?sH_mq!4#nRG@W)Fnhh~zZtStSqjPpQpNnE9l+;a!phQJ_8})Hjd}D}d{de0T&# zaB%W&>c0(Zvhh(aSwGS@HqZ<`UrR{sY+763ICA0{WPBbX%p}5o zN!T#+##XiGd)War4q0y-tcKvAdGgCe4ADKhFIR4$8dk1_A@e}>x4^vEjD0UX?TACW zQgE{Jw^McAvM!|vY4FF^JSf?lvFi<@dwX_#vNm&#@D(xg_6lD>XgUEtW$+EDH_$ox zr71#t{Da?ML+n+P`&+&u7|7JaomAar?{eYwTDdaP>^3aDqqoc1qguQ7^p1fI{;sfN zUS?{2JyD4WvkUCJK{y|0ErO8u;vWQ5-t7auA2Q$1v|QO6CpT2yVAXv5Lej>Ks{ z$Ld-{)N_`S!ELDcWAz55ArLzHbKip4@{mZhq48?M)+=|bf$+xCzfOOn=7CV|Co76D z`}${J4M}!O9vAeiM!?kYhwh)-@@?|rAizDH3I%(^ zpfYV$cuVhTPIvuQkSc{~aNtCgdkO&ng2ly`0XNLNrKihTQDuYM)5x(Q_V%)G`wGa! z-8Thp12qIJ_VXMr4X;|Y3j3~sOr%v0roZl)pDkA#goe@Ug{bs-<5Ugv=BW)pL#KAx zqpJ$8AB7)?c_xS&huYWh2*V}@YHwSYXpD|H6%I50MCL?IJHJ0mya-xD^jH7jC#uym zZ=s^^y$QImjnuVZEPM9q^A`ddmQIAO?L9AzBhm=o9^By#!MA(5&Jo+$fd%}0`_7QP z;qEPqFDccnTYHRG-!}ZcdyiK|TP8%^rumxC299@?^n_fsM5e0k3V$XX4l8L2HR7ev zK!OgkP)gdl$6DL3lx|hM-(JZIj~Ll@u44o*O7$K_tKPD>_2qLwCylG8+1= zbV4}L#h=%XcIAGOn3X=+!fXgXZ?wwNDf(!lU?1U<2(V8(X{1<#fgDmoH)P)VZjuL< zRYTNE_>>^=?zV4qq^XN`)Xr4mWez&ivzb)xjg}+AX%Hmd07N^1Qz&Ew;P6(7S2z4G zr!0Unk<{xqPvAF13gvU>`|Wook15(9@s^PWau%&OtB07vB?0!JdYLRNQzGW+Y!Dm* zpN#ngjuj*~g^~K&gbK*xnp-n`hNcYo6>ZOx!9SkfCbTHE6`tdQO42jYRjNGY!9m>n zZ=y+$pmkX<>fht|oYc`lu%1*Pf^$e&^Gf75F(llC#HDWy5db%~=a-4g!fb>H>2%GcbR=BI;Pi86^( zj6NelB@6QDbC)$S$M5*la84?3D_HyT8vN4!8M1%u``2}-DX~=3v8NkGfWeF`zZgVZ zIqYomKTQ?hhi9fedr&JDOh1dQLiUD~3!0#Vd+7TXS0QX(JaFxZjBP-*smsxg3|;t# z=aF?P@>G#k5U{paewG{R=G8t3|L6!+qm|ZT6sxN4YM%(b3P8LP`uj3TYp9nt%7x#x zeyOM@eZGEqP0Tx7XG6-W`2ok0$Ex29timR|@%p>DUmzsvICrz#hxq~dY5^(t>{f4pQS9&wtu$RP@20ur zmY*&ea|_`9)8L^*%C)QnFG{jA2wobPmEi!%0+TdRDP-OSsLq+VSbTDT?XcjQ!>5Ew2( zh3obogKE39E^qbJ7lxHt@0S(88^R=iY*q*+_09gi0N$`>8i=G>a3X2!@BxC|(8j)* zVyhmE*xPPCBN>61Y1cQxUsM`iJzNu|q?!_vL`Fk>ZG@E!{D#WutcEg~l8u+Yt?jhh z9We=HR;CetByiu{a$6wd0}hEJt3c{4BkBES?f9ucm4*whpXDZFYcbqtqxlbC#ok(G z&s*QVJkGQWm~qVO)dZ>g5bG$ht#B9ASJSU{F!U zVZn4vKyCVQKnwM!>sUK^%Y&=>`RBlWuh)IkQa}5MNz|*T$Xi3E%0pKcR3i?WJshTn z%`@dSZ;#nbPvJEg@Ol@@eQQ$5Qw)NCWYBk~pG4caT+1IhDf~KXELoTSc7uO$59>AL z-4&YOp0NPJ3~1UQ5Ju`ormPDYz9M{Qb^ox({84`1EFAo`a8sG#mYVrHD|~2{sxC|# z!|n!dc;QnYhMZC{O;8E1voRaOk@%p)kbbixjH5$VKAaz-6zJURmzQak$F$p0gAerk zDcx<>VCWq)n|h0~+Esh=cGp!VCYO?s(b#BJez`Vzw@`|ADTcin&Ce++?kc}35A>8& zDRhhbm)qbCEkBKG7lf+qoBc!q7Vq*Du=SGmBt+{OZp%*Dr}S5Y@NW85K;dn(U8>^6 zK(#x}(~x)d=znxVEEU{T31R>7LE8;*CVJu%*m7;cE)>894kY>~Xyx<0t1#@HVXJ__ ztBj?CQrGn4uOUzD&(uq_mxLfwP2vfgj|D^-)jU$D8I#x>_g7hF`A$`2Dfd_Pz(^nE z-?w9=hTSeW_+}r^h(8Yc3uh!f`Bd>jvxmB`VmRKGb!=$E3^uM&X+Xdl=L~b(@o0@R z+*h;j_I)`BR`lRMrq3;2yF-#HKOk@1t!(%(8@*PU3#cU|NhW_!!2G&?KsP?69A<_Do2UBSAK-Q5k7G!xuc|QL})hE z_1R|&e~!rTW6~Qp0hCnO*!==TrsdZBJ<%PUZd|(G856SALh$v8tOvKGQoQD0_UExp z(#pFY8T^Bw0`6f(nL-e?Zfx0~!U)g?4NiIsrZuaZ+A-;G&;Vb(X2l{zfy*2=OLzkF zY}y`A+VH|-@af!($~s=O7c3c${WaVen0Hrs|AA6TVYQvP&w&8p?f#%}q_J6D(!ZMo zQiC>nzU`)ax1K~l-I7YW+KhSW54X(CLE+EDds6e;xqt3_Ph|OwY{0mVJWfgqugczP zVIZ0sU-_ykeKP3r+m9J1_o^QR%;(}D}_nX8o=b~rPUB)VJ z#lJORHSmpE%b2o1*9bom1;$D_;E_=y4gtZRf&GVGKiQ?gW%@aZktaOo#U|5D7aU=h zK)zZH@ggVVRu`49S0cD~Wf^>#C8|Oep2X8dj7>{RAGw^-v4O9p_{#y%5Wx2*(|Q?6 zpl>(zR!ike%cinft;ovq8G?&U+Fg8MLK5QH$QP@q0<^mF7foaG;Dis;q3XMz&31m; zSnB!KB*0z#&+I=&>l@<8cGc{iM2jkDnVkQjx~Cwua)yC$WR8o(D~txQrZ4}Z0@2Wk zbI5-mCSdJwOZU&+nh@LI22@X>?~CXzm>XV4G9_GRu%rM;J+f&ZfOK z1r>EIc1{1f4XYq!IBpa3^~>-=EyZ}Xqi{%H;l;l-fbs4!FaCnQk)zte;xhWr0hq%| zrirHE&xCb%q^aKg%{io?e|8%56lM16nnJ-JHHD`KN`rb>4laP^c$2o~ zgp&^RT%M^b4*gEbOc-Gs3Qoh~8w#7yB^M{&iH?#Sh9=IXitwM|MOHFQL*vVivKt!0 zCkuZ@PQ2LbDEkyJx7YA5<(JKnwrL2j#7dg+3qrP9c_rv*-@24HGKoj`rWLtrQA6P~{?@v>{hJ9kLzSM$*Q9bX>XPqq`;4j}M3-zwr;YO37#V8I1Vy*E zHx7W(DxLb|jOSS2QOF3j}T0rFsgKE6C9tkG7XZaP0DEx$*Eb z`7&6w;Rc}`?a%Q1JR?;=%xD(>>TR20_+kc>&jWn8aDxoU`_@g1On|D}-Y{k#xkk)q zcLzYX-XY0IUR#B4*)d2I@!dI#k-XNu2ja0Vp{k~(nGr@}^BX*_hqnkTwPw4zkrli9 zbvYqdG)S`VB3ul?S36^mdAO>5SHUVA#=yl;>AkgMK&M}R6%^ybr6$l&AerGR6He2b zl&hB3>$Qo*R|>g6w5#pLf@IU9cNn>U)~=Tra9Lbtq}DBl!aD}1hTm`7?ey<5L*dmb z@5_m}0g$IQTQ>$x_MP+ms|qtLcL%CnjI1@$=>L783hDO2KPrg2%=-edk>@NHH&^mz zXu4{@eA)N4ZQuZ}KC)`MwCjZhUQK)~6#hu;Y6t%}lGq0{Ee3mc%#t%~K4%&c1zWt} z{^gvdU7iC!v4!~nj@{k!`VDsO{D`mmjIYrwrooBO&Srm-ds&N5k4FI9w8F zAj;e>dW6C|=Ba~=Vc8W}VH(SaW)o&FVKM{e(fEoKl1Zrh(>KGj+ZtWOT{%h#DY!W0 zlcDX7MtP6@H)n*xIEZww4ID+~V7;4Rc3|_lj57?ZpiU8dmkg&ad4$+=<`Q}wX+wp- z5air|sGz6Qv*3)c0eWsECY6i3R2yFrnpDF`2y~-gX#9+YJje!78byw*2xOxqk5wX_j5@}j2doze8ta9$>&*+!XECKek7;CW@G=@ohPrB1o zw?g68peXI7q2s97RL@`G`5SPTQ$iEI*%kiG29wLUG{z`GP-$LaApgSW-oIG|tHH$R zU+)IUVQon&{cq#or!uIy%A0%Hpb%t#DqbdKuo$Fq{q1(>jt|9~8EzsFQeG2(l2Vd4Xbq1CJ@tSHJv6jBPMZ#Es5-b${T zV5-~a(*5#m7=}xOEIyv{z{7k-Vg*`s3F#p)G=qnnfz~ZO2mENWI_i<(itUKid!n4B z-lOD>_@|=FsHARp&2S*Zc=_iCA-9hlSiSVzr9A$y)9moO^mpotKxL<;l!reQe#>B4 zIiV3!#$JA3)=?$enL}P-Wkyx^Vg!%!D`LO7jkfMtKA*V1CzhthS*VDm8U!OEL%R)! zW;)V<4XRvx>5SpdjrOafLf^lK82>py!fkJ`V@CSI)@TyHl&E-Hns@3Q2ebmVIdi<) zIHt_!%HFWg4Jh@+s@{?;Oo;|7YK!tL!qft5?g*{8&vH*|wjn6tf@m6{+^D2I9J&Q8 zVJ|-|zlLA#C^lmn-d4gGbTIP!&tvsEs2VGx?caldD_1%Q-n+%M+M~uTf1-qjTe)43 za}DNkqOcV(*6tgrg*B*JYjrhz0~X@Y$C|-4TTlQ%)?GbQ0gFaa5JI^8dx;>mId8o)(vmVXk=cJi zu;dE#^516$$pR&i!WXB)Z7r+j6g-QU^WlDjEY3OT7&>kbw7A3m$S~)!uY2+W>%F86sPzl63`@d7TS(0*dPQ>q z)VUvjB$9+TU9j{WwM+J_hQ%N*gr=DT9o-jlcEWxWD()IP-x0L^4H^qq#Gz=+O9ZW* z^;eK+wmB~Q1v*=rq2G2G*?zgv$6BkCye^wP953P04M*8$yGO_LS;K7hb?M$h2`hSt z1MKKC5*W<6a>u4X<8{}%{nxl{>+*=j28#=wYhkKZ-Im5KuTd9Ll-1ld_xV)_wyj>I z3&U`c$J?$7!FJVr8kD?gobRPkk^V*k0TjgkvogUN%G$#~tVAIf>q*8UcxJScI?!=4KI1?DqCDRd*jjbJ&TwDxvPe(Zb4r4Nk$j%k zs=(>)m665y7jY9ht!mk4O7T(coX`TTEu@T)m5;Ys_(_DX3j0;vXd*Q@zqMEsw5m;jz? zT3+2hKC&>+%rq%Jp=}*czBEvgqgLC@b#0-@F6~nA<6vr`q(FeDE;lq%2`bnNYme&* zS~;oRTTCjOJjc1)xuKnC_&Q{@95q=^-Z#13Qj}zG{P)E&8e2nCM1N_r(sN21ek829 zqg+_G6)oezol+5_Rz~h=Dk9TBUGS8Z3LRIE?QbXA{e`pe-r+ZnoB77%r#i#mc*=4| z$BnQ(kj~NkNZ`eKy+{kX%~tonABeEV)h^*G9eyVA)Zta3=!Uqr<9sTsgcjz!p{+vF zoqb=}Wc8Q$9j?j0Q$z}m)+J3LT>eWm1jC$pjf19PQ~lqGOwh2=9|7DpY)ikJecg_=S22PwN_us~b=bw*^5Geq&!xB4LLrAAa0|B1P%3u__i@>S6wH zTg{$ZQb;cB8Oky74H>mr?RjKq0yPB77O4B0b%mlEB(TFOA;RJogpL1+&FV$ZLWT0a zwK%@Q^M*OMajJZwnNSQy(`y9Pyt%a}+2pmG&)LP!tO`9>iurG&wD4vngK#&-8OHH^ zQ}7v3DBD!%xnt%eZHeK91lea8`Bz*##Ok443di}qs-Vj~@Ax421$2S;mBrc(MK><# zFKrudElg<8b=#!e#bekTB5x+Fpz8)R@H#=-;5(FXMZ=`-ckJ`$)Sw(Q^2dz|N%xZK z+5g9EESVmzW+&=wAgs9+#iH>0kPsl{$m85ml~s&RLO*MQtSe`~yiTOj7BreWC#xWCXBFh0@#F%$8m|DYfAzC^&V+cKenjDJz5m zOT!xmk8UvL93)RGB|Hlpm{Pc<==D>l-_Mc5zi?1}b5yHNHu?+$ z9Hv3~t(Oq*{w;%W^QMSa=%2(RA}cm*ZvLN}1+!g%mRpax5mDF-#lY`)YhCAcJF>jO z)l}`R!4N#2;BTO7L%TyOcvWi8yU#^kkzM)@*Bj>|_6#v0^!n`{E@*y+dTB&1NU)^s z4;0NHyBCVSb|)zcYPaw#Y-P&iKjgjPyyeG1^lc?wy$0Ma=fWZVqP&Km zfg7B=Etl1bwx6iPDFz?<8)-pVBd=oiuTJ9YdkrSu5KHm&#|mdzzrh)9Pe%gWrFF&f zwQ4KjN1{Qk)=L+HNca-M9{$n^*nGX9h{?hU!nX_kmsh%%RrCZ@$K@yPrV4N=0UAzc zMNyvQ^5^CTFYoBt@+6ggQej|u=T3tHENAdj_a~XtKrirOP6Ks!w2>#USJt^EAY2-Q zYFOJP2%}>O6-Uh9y9Nud;#gkhDB<*4Alvd!!GzzMDguJTQ6PHrho1&I1Mieohd4IH zau5GWz`(2J;Mb}2n?GH=R?<}a<$1C0lrKTihxB{~ZkVvwHAF!(xr$!}n@Yd6^johs zn?=2N3tR(tcco+v)-xKdr|r9e%`jJD!j?}LTJz^z;Gd!CzAJ%Cl7^1U8#8?QtRYoR zLKt#@qs%iZG>3N-s;>eE-;$Y^&M#|(+i9`(^vdeB#;97uz5qamJ@UX5b#B8n9Iv*p zP0vH-=|>Xl9IlBblJ;tH)j<6KAP`1{E6^S z3Z8v_WCJ3@AhKOISU(%d+k1HH_2a zHFvtjns=4{1XcF8HbD1Xlro)%3aav#hJLKpfg%e`hJBt0#I>-)CIoz4Q4-!(_Oh*M z+P|e{-PYq`jugLgGi*;iz2#j{DH5qLw(a9TfgYSBNGIXoqnXJ)={WSl zUeuz}4)=5X(8EbV7at%(mHa?`QPfxaX5=OPg%Iq+-L#r@$*pJwZm6}w<&XwDcWzCR zZJD0=y@jn!C@^d1y%Xx8s$TwV;Oq9+Rt9NzG9c7y;ZpoNYP@B~2)RnL%e~98!NjXp z+_(Fw@d3Os-YBEzh7k$a_tB@w1BUH3Dv{=yGmceyIr>Tj%JcYqi*N+fVR3!Ny! z3Xcr6)-E5e9K)3J;vb^nc5`YS0HR z+L`3=-GfF17DL_p!ndy0vl~w~JER;vw`K)vbmcgv!^NH3i@M&AE?3e*&^P}*L%tM zQ;f7K1>U*Zf!tk`IbKO)Yt^{wAAX0(JDv<2=C7HeRA&~L{NMEC1_*@eqq-%7JG5}m!7EmE1I+&6<97sCS()M%OLiIyQ_Y^)D5En)sd{w_yy&B3U9Z_i!{QAW1wfv5e2lz!iEwK76zo7LB4k~44 za9Ic8_ggJoi;sgDJyTO8O&tX+TOln~Lv@>2XI&xHo4kzviDPK=Lp5TTbzIfe-VOw%{`uOJE#kSTCWj4CL5B8O{V}>Nj zt_p2kBWK8>mML%B_Jy>r3T-VrZ=M)mOG)gu>VPc-rV&4LOT`(feLns?-Ny<3KcZba z5~GMH{7wW#B%N9!yDaP4ez-y8nZrpiKr2{K+hbbI_ZD5fdE_ZAE4$psDM|4jQC0)g6 z&DsguIViRg#(hcRGGlezLPD~goF>v}$GCQy2b4mG8+Y$|QSI33a?|&=^a+7~rj2tQ z)M^?OUI6HCGexGTWdt|kHr6M;3;#L6nLA2- zWjD!F&~A#La;$nf)X(fu&7=!$q#fv7+IhZpRR`!e4zUY7ODTCI`FWkN_uOFzxv)Up zHst)J?8Si-5;mvB2n|-2$IxbY3~N-vC}kG)3NfzPFq zN70DRrz30d1odssgd60%nsPD2{tISY&AvQ!Or`gSXAYfq)n`iSkh3y;>Ar8RZxog< zjhsVG31}MjUk%*1GvF(pW;40}MNO^#L<5ahVCt{=@anhFn!8`ZpwEWZpriE{sRRme za|4Zjn*W{J^`-?24gws^w{eU9%+c-L$n<`1IH1s$mG3JwHcVS9i}(DGVok0qcRGng zKQ*0p`hkp_nq~-TI|F?!&2FGEfL7KN-*qn%GLie8btg`N?yDyD$Py ze>`7nV|WL$$AI%TxR;I}S&*E~fi5;c(GB*ABX_8vKCr?}fm}4jKt5}l$XKbt;4$F1 zU9T~^=>AiXes~1xxc$6x>9>QvUqsV|p9SDT@!q)eC0o|j;m>3P#0AmOlR7G6QPJ3V zHw>)hMz1?70f|h7c7;C@0xpkBO-EFb@=Wv?KrZZ_V<3kjI`lvlM*?x_S5DY^h@UM-*g1z!lYaC1HN2t;gV2vYg$_nt%*>V3v)FPx|(x)dSA3@ z7(gW7X*eptg*~4q&@`+RJMZWk)dc^n8mLRce%q=MC=b$0}8Kb%Q_2%mVGj168gp5hlPJ3pw4rsm-zw<6@N*PQU z5#0R^g@Ctiu?x*efg#9i7*Th&OY5L*wkmsxVi%o7_{p)icP@>Nz3)!F*qAxQ z!aKsvK`4DsnGfor*PZY1rs08bG@^OwkCcs`Mu&xcCA8m_2ePq#Rsal!`4Qr;nzfD^ z5`XFE)WSVZ$JXVv~ARdnzwR-gQ6P~uVoJvdO2e6pFJ}SM-+u{$}z!sf2+S2 zr8eqrt%O|{fD7C&amU+4J6~0jM)ixJT+OK5eL-QskGlt5!rQr-BR71ra~%;L&xYXw zy@pQgDbNc#!IrK=41-eVm87<>y=OnLRk7XN~{OHqEiP^gA^3MSpZ)k}= zmidNxu$OR2Xm_%=e6O`geTTIK@83_Ckd|`eYPayzum}RZuwHTIS`XHEpYYMbj4N&5 z?^}_Ad4)rU_xNU{t<2n{PGdc^#&MuW(Jho&K3jhBoW?@X$msv!J#CN1)UnUb`FgS; zbVC`^oX1AhdW~0ndO`h>W;nU1xXWB~i9?yp`mu-N`q=ds8UB(FDIL%{;rhnS0itf- z`I%fpaO5*u;cEw0H*$yCc~eR^?xGtx;O(iesZJfSG87sUhA|eTF(G9v=qWd96&nbk znm=(gcrO3WcWeEM-n?YxBd{qWzR z&OMQnR%g07FPaRiWUUZwG>LvFp*NpyGefV*X z<@g6SoI6t42*nSrj(T_91ZkmfzSuDY{4FQz@I4V|WAxrVEf!N{qWNWWA}_7_HR)-F z@5nM@jsq7a?bP~F(BQ951MhH|;OvGF&1XzdG3?CnbK5knmov(^8kC>`Os{fG7MjZo zx6d4$+aDVyGScd}1rMYmzFyF&2DgxFhq zmU1YP=EUSjUJ9FV?zY5sq1j8ILwbE_s%@7kcNCHmwM7XTE&QG1rxL!pEg9DJ5hhzW zc3aKABP9cBzIXtXPXp`jm2t7z@KU1CxC_FgIbZemsX;S(ObOF_!<<6>Z0bSDqXsnm z1YBX2c{$cBW6Su$(`zfTN0Fz$#t6F^0^&tE{w4AM-gqG1a3LyN66B+2K$*Dk>m#nJ42aS9*4u{ewjnT+; zmBQ~bbJV`<{;b{+n%*bq3jD?`wAe8{XL#{qf~70P>C`=hD4ZBRFo9K=G%kXW*sm2Q zXAyPtJHVWfJlt;Dm#B?$lT{v}OyCO}HEky!*`_-qZ~(0Kj-H>`-d{Brrp9d_fHQ>_BPs!sv#HT@hfwhneLM8$!uLsD-5J#nm9wc2DW~I+cJSy zSgl?toP_g5_w5r?h1}1QUUK{ZnG2$Y3tFux-x#aarm~6Se_zA|w}r=rxGt-hdcAwN z=75Hu$D^pItGKfj_Ky?T?mjNQHnRo_J9C$(4}N}$x-H!YA}gV3G1cEyFc$KhCyiZ2OA3YE0T>H&=h3(I*N)|h z!E>PmaC0?$yF}u?Y5jE~B@&TZCH0CsoT(u0_B?D?_1!|xeyIt4w@^<*--TGIZb$ZS zIe5Bb&kNW}>K59;N8uH~hGylVj8knino%0&x@{%ub)hQvZV_Bz6SXZCYg}CP=D>-I zKRmoad*gDp%BgWbM7T}>bpsRdI9Z#wh5HD9>>2*b^<>zUjwgthLb8@?OLCY>>cqhh!t2a)pHJ@`{0P4dRqC3dI;8kGtm5qJ;tOou z7G{kz508Pw4p&5C{lyQZIi#9B#68>@yfQN@a{tw7J_l;|F)grlXUUOA+PtliRmJZ$ z7jO)1L(d99&a;%YK4eU^470Wlj!YN;~#X1i@;N>{(Ago$Mdu z-2uSka^hP*H0us89J19yobREr@PYwzd!YKFKa^CmS6Nz2R-SLTt$2IVI7Jcc;bW#< z!$8=!Smby!rpCllo^{Stkb@f*7>fR~xqw zPt z^sjCUmqhz?QE4}w$iQ+p+*9PmURJVp2{0QC*PO@axXwH|sE=0oGci!0a+$HnNLtS~ z5BV&h#DUv5pNJtT=-zw`!-C6${QUO;o*Xvis20HV9VKCl{jDl;HSk-`eoD1uNE8uAAOGRP(1FUn&|Vs7FVRYT{k=Zf zm>*u-MX??Kk|{1+f?XEZb6^iai0=-T(W)}~CPFkSWa9qM1K)SobVABl(C^cQ|85CIdwKeofpn&1es{@!KE(YZv>DGt zjD@hZcA-!-!J`p9>hZ({ZwDq$GdK+Y4tmuE?r*TYc+&M7T2dOJ8Ui%DYHps6&JvNV z{*vJS^1dXSRCo-iEvH*Z|1M#U73o0JPd=7~tj&EP<916x3j6Oy2xva>v?gHPh)%{yqKWsr_AbVyS*cW`A*__i z^Wit(;C^*=&!nHvjnN|F%MO=>x?sO?zcn8NT%i%x z;DvbwchsDiQC3X92I2x=bv=FNq)pdBM+pnT`AsKm_}tMOuCNduRpUH51+N_1OS%_q z6AXq5UA&O$vJgi<(%)p>R3ja8Egi`oVv-HUuaJ{oM6pT))O~DISQI!Bge*zoleSC&J`M{3Cl<22ZM9a?WVONnPca z(r=;VQrGu@l4|!Qd~qQAa>j!C+ZrlJNy~Sv8&uy?)-9@cm|wK-kv5yhP!gLfH95o` zmBSBZK8Vot6qxpptC1hXY*_RP`v2xYUyY;U+r|ypH`c53Sq{L}x0V%tCf=6SE_r1p zBHBCL7jyf*WfE0z`?>m>XT#-T`mVE{@;xn<%5p^S4Wr(*S8i6LSiBUICEOk`#HC$* zCxCWL#D*`;H_%_@wH$}uQoA%Q20ZGwE4<)$e`hUrf@Cugg*U8v_N=s{@RNE+%JpE|{?bV6a zbI{)L%})>XZ6LjoXLaPLOdYbe@VfN&@A9IE-bKbj{@4TkHx57Oxz2K3L&4Jlxddx& zI^}7y7X~uV?v^H;?=S0P*;PxZ==wUk#pAaUm?w%qxs_1R)_y^j7&u=Itj9f|EbyGr zrzc0jEV72srTy`9pd5p#(*zKq)d0#{D2vG!%^$d=Ji>h>Abm)6r zn56PIqNWHprvnX{Ig>>EyEM8dtTVsxnzCN2+JGvpGyI9#9@Sd%BBpf93f;blQJ|Mb zl6Tj3evlWG+;khdTSl6QDBgEoR=Pp|rPVK^?hf*PJW}TP(oLUt z9Hmkv&uH5~bz94;YO!2`ezJN6%T~@WCtmBI2r7yk(D4mmSBl;w0ks*MF(?c-U|k+J zLz=(comBY3PTakL>#E<{@k*(2E~x1qecFI@IZw$2kYClp+~0jgbXh$5->`oq|2MbA9KTfdM2TW{ia%a@@Z{yLC)gN4v@s9enayM>zHTpPTb(~~mA z+qx4!XxJN2+{^_%+Hb>f)xVIyZ^M!EM4dFlUN%S1NZ8v_vR*bvH67OKdxWA0#toZt zc8{K@dP9qsO6&~_O}<$F5xm26Eaw)2F}3$?^C#W>hxfDGpz~^Q(o3w6?#dcP6n^Cf zpMR$vo1&WXR?F@CJcM09sUwZEXP(+su$U!HTF0D)bX`N zdq*w`(yM5ea6QEC<-sI`EK!|-h3kP|H&a(8DZfChrR^Z7UEt2!d&$+#jJl@!$@ea> zY!JJUchqrdk*TaqC#%>6F1`Aeo=}ULC=_)Y^Di2~*Dkn49z!ht$Wh6?+OX+W@oNGh zfzh?9e?8&ZAnwAM9<>35aFg9#%&;M4e=*FY9-zMCd1M>pPN~eHyYM}>_2rl^sbbO z2eVzMd#8WPVA-(fh8fE^NfX`JZxTk|<#8L75B-Yz2+s~Ky}5m{C8r@ZCjdP_!oU2L zNWbajVxm#4-)ii)m%k1zF~o<*I4^%w%(Fqr1Lu54oxhb~DAVxYvxZ+oOm2QYyN$?% z;%^5q+~5aW&yZ=g)$xj;T@=IR?rE@e%-Bq-x5B~>MIk5UD$)tNPMpI!fc>vxd^B4~ zG@RkjvqNa*yHPKu)D6#I!GmO_U#s>r+B{MCbP%*?sH&9QCG#|QA15+dGq_&roM{oa!@nvp zLy!h~?cZKj-Fph?wvV3s zR8_F1suv6HVu@>^k3OYJDoQSsfEVxNG@sL^6Uq?t&tsc1Eqxwa z!yQzqTYM`8R97UD8Rb~`EjsgL|K@Cu3$U7kFi6ioFE87-nPZwJU9qFl`r!RT|>4pgfJ&RZV z^hcpzfhTXkhv;cZC^UYRrTPzQ?=PPJtP6^{bvw(kO$DYND?c|`!>AQ;@f8dyqFYThh8TzQymlRMwFje*`Hrn2WQ9 zy8_?lK#qN(3kWdhk5~eZ%hWGUp1h0)Phq8&yVbAUPf&JZc3d2@VH2Tr$vxrAr*t8Y z2aY`pcMMM4&R_a(CH9t*OXl`hz~)o)Ho=b}NEiDF=rZgT{6ZkQElqII5u-L( zo`Jdv(QBJ;Y!UIJ9Fj5a9|)+qJ55GW>mR&t%OhF@P?s8w}oh@G7AZmzHA zX72t4tz)%kv0W%a-a)po!Q^X#eg&QN#^fOdNyd9h5e4xn(BDYRQg zop*OBy>ZynOf}FmKMn^UE>TO*7NRxo> zw%T&6mCQ>ejHpP_-Kt#KH;&6RQD*n2E_k_3FwoOeSHHRu_66hPmNd^>J{}Cvrk|Qp zGpbzP;%J?u?r@{{YNA0vl$b9o6+}m9-UR1wmA)oxWhUzubq^MAxQi%r^F338X~8(& z(?9Z0(%MX@P{mBscl`7Amo$DnwJxxNd;&XEWpLx&3Fk^D!b))UO$K#PTuRDu9NM-C z@a1pE2inb#bahx_<+M9H>P>Lt<@`PVwWhKexP>t55N_T=Ry!QtW7sXsl8&IO zN)uHP7j|_3yAk)APcmAyoSv{P0NLe|B4&z}!(g$Je>b~nTIuct1yy;dSvy>TW=b6< z{`}g%e#>7IY+MQG%z9dZUliI8CF`Jr^Zlr=bkK`&nr9#o!qs8{TAv65Lo?;QOE(!E z*H?pwT;GJ4tA!tn!zc`TpHgbkQAlUy(GUV1x2<-GN`uOvdPl!aKzL;~eM-Xgmdc*% zS^Jv?xR!(SKn?naYK9*PY`67`#IB)33eyq-w!8PtJwzq+1;iu&ii3z74Bei$MMkL_ zxaDb1G|?difWDvRd^)rmp`HM`yGm~BtA@ZuJrKIzso=PJvwbQL*^CFnj*pGv1w>Imz_(T9-Z|wTzPy(ittu-o_;A^8roDPrn@!#?-J16 zG5)J1)PP{-HcByJ6iS+X>4(yc$fAu+hYN)NRJR$!?~8v|C(yRSuf$w>zSPnh6bN-xIJFeTtvu;H?pki2 z&=);Th=AgX8`iYuQP=7Zq{A~@=tJL5}KHYAS zggdM}Rr2EuhlHoT-}jh+$L-VtR*sO0Gs{=d3K=zx!_?yyraYWWE@G z%cOgRbs#MCtdLeOk}03O~J|v6f`~F9q<-L`Y_sO_!4gp450>JhFYzEqTc;> zVGavP1TEpu;~q9cqvr-v>Z1;4n7717*zm%5BLF&B_~4sAuBZ?$Yj)D{E2o}-Rha<( z^)xHplKWLO0dtl7>3t!*qUZ0SvHzi zZIFjmCDZL)je&m)ypU& zAIvklM4sV{29&Fri?7K=Qe~BgVccfGxcU=cvRJe+3Ri(!uJDA8QyIh*py;8l8Wt{{ z=4UfvZNpIx2Zb^7L~3nOl+6;h*I`@9GZIa4Qx(GnjvXUHEjXIBQ(6tdZTy8GC#-kx zA;P5JhlTzv0V`q7*GZ}gL%H|x_naYE^wh`{Ow@LGlVF!1Bbxu7)2#-&b|9qluvg(0 zfY8i0ZG}AmZn!;&;h)x#;>=N_hs5RyI;F6ZU)=S~ZL)eH_h!Jd987!Z^s>^kaD>kh zuKX_E=FWp=R7!15gh{AFg!&ZJL#-n=I54iE|ksRCBeZJx3IV4 zQI#t?Tn-pHgM+(9+ERf1he9~K01MVnsS8?B?;qrlh3CJJJf4;s(1WYhZ1FP$US=7V zsza6rJL;rApb$W8*E>x1rIk}?Cy|ym!udAOPy*~R6mAR#LEI1L2KnH(K&{~km@>X3 zG=yGo)GP|CFkYs1fsS~>25Wf8n-!?7(ar2x+^bcrT+{Psd4q_M@}SLI1zr~Z!NEZ{N?65o2=sW*jN0WbexDf~oY>@fG8G2Kss6OVaVsH69I0@jspNb=4H#+xm9qCsrHAl7>Yh+i z3uepXRZ`kCFciv#;um2Zjz8B7~ok^fcRS!7wWkbPMCcP`eg?VlHW>z{8xQ7 zvJs)olEJ87!sUo;+$Y4!qS<5IM28$8IaI+roJ+7O5meHyeM#R5&Ye<%@Z<5R!x%=4Trfxo81sUN0Y1eWXxZ*;)*e{a}TIPKopCm9jrb|Yh`e*y16R zT5Z4Cm)~08ILzS_1{+tZh7-(FBP!5ed<9e$#H}SbqC9zvaz_OiEny+1&zyT4Z)Z8>{4$)G;k5!{yLLVl zhfRe7D536z1#asvr=5%uroH}Q=P|IB(@KXC#H$J=Pi&~&Oe!fiG&P&(G!46_{R(U4hb8x;S1l_5=?$n-0u{(n6`(Dp@k0!WbbZdIE6BFNk>nsq*~S-2 zVo+_ze8U1suv-M#hBFEt3MyB9vwc$Ds}B4e&{cukDzcneA(*G{^mCbN1l#h$Fw@*} zoAid=d>{vJ=3u3g5s+tC!)*jJyJxj*(fNTcETNMHjJwv%#bjdlRFt{of2+AClspYh zotd?0^ZbVjimez4XSNHx%)V|oE>Q)N8z!$OuLoZ2KE7mE1#+vB@Ln%Clt`ziNq9A3 zoPf2#S@m%LL9^9@?~Qm|Kf_51#|U8CQE~;an$s$qKp5t~JNef~Tzp0eCJ#Qj zbPFl$RoWlC@;4DCu&#S>t$j zh6>cS*SGq7D<`pNM3XyWBgZ3*o8Lu+9V zA=cUmE~A;#DwY~E=s<|V9|-fFc{5CFh>2&Z3V&}FP}`mFmaLH;u-p&Z9$?!p*yh_e zpcZ=y69-m%1#&xQrZTN46eHLGpM>?QLQ%tMeZ$1i;`^61kZpIHmw`?Noh=qp%~0DL z)sKb&jBz0!o%DY4bYV`^ivOkO3v*;4S;|l(P_}|{w(rZ-($XNfaQLY>dRA^oJHNw^ z;N{X_nGl>Gj|@*!EQU&;8R1s&uy@DS3Icxh2UaUU-?--fVp8oWxwMDh2@CeVZ{uS? z1~mM!+{ohY<|~z|Hju&t%+Scjhb1K`DP2L6kTbqSFqhI1O-Zd%4N)5GA zgKffk<@R8|=O;kZz_=|n9b~Xhv2aG}U74(+XFIYd}u#a@sRxuyyx5Hk#;=_=z z)p`UEJg&L4zr~~UTaMwjdtXpGIihrc<}3PE;Vm~Z1O@3&nxY=u1)Bw&?bH>Xzxxl@rc8cVW@fqr`ReY&vsO}yYYbNRa<*y{a_gk`A8Mi)s!?9|5a$7^RnWSp} zcfzY`LcYS?1a%>`b=AUBsTIYOk{c{?N8q?ToyjVsW8Fi~&D<3Byagl-czXuB%mz?5 z*4@ucb1~j)+H!@J<``DfYPPFi>U!Gcw+=6;z-By(Qe(LU(5H115@psyu&>oVX8QiR znXdx9VZurZttzze6X6C&g-DTEr)j!PnEbcfZ3aD4bWxnUHC}N0HycD;F{U{?-}VtK zg-F8P0Pc2Pml=S~IBN04#0D<~%H^A+AwL!f>i3v&(p{cbmY+nNF~UNK;$PLas{F$Y zz<)~vgzcbLd*<;rn|Ic*5D@QpdF0Z)z&V&kxR(R&5~%=F;~12X!Qq34iNa3RHJYCX zidhdms3N?773xg4q_5N1^BHUqE86Qgx1m+4p)62vx31tv_^i`D#(RP>{9epSM|3Rh zm&B0%02SMWccnBGFZfS^^@%5)3yei0~Sm6FT+Wne6WnlXq-{cMQI` zYG`+0a7u45|4F}z0n`l>qsNm|&X*4DLN5<;Zku^fqxvmsF;T-CZ3YT6eG0I81FgG< zl@^;g-)1DFLj(~ngu@8L+}<)xl8x{oHDqFIa>OUft$`j<{=hfMGpKXXN~?jH+soq~77c|_LT27~2mpl&y~CxqaXr?+B75_0!Uqz3PVozE1fs@TMk)2myWc_+NNRTgCfqO%A` zBP?(w%k^EerEbLXVf)qs6x`YKgh`bjs*x&%g_ctF;;pCj5KA!Ta2JvJP*Mt@l==lw zO)%GM8%ItZc`suGIpVM{E38Wjw7qr~Y=&aNou*r;8Rxbm+TJN79Prr=6I?M=ef4eq z3<8R;ggX+=LKw+<2`bQ|-&cqoOx#+^#h;spv71X+i4Lgef1$<=Xvlr_p*93jP-gx>SYlr1Q*#0q0SeJCS z+KqNe{+0l}^_y*b%3@)aTz#wCLAed)p!c)75j)%;}l-wAi|#qklz@Cq=raC}rt(%diY zn4aM}jh`OB>=t0wFR2;=(!+<|-}MTIeo7gUBE$LY2 ztz^06It8{O<#DwMm(_B?z8eCPnf+L(1E!5(SnF9`;+h;@j|%8**GtU%z&qk2(02z? z+jnN*B1(k@;5+nI9VQ7Q&)k*50T!qf@+Nx&m1VqjWim2p#*mw?qwcW04K{B4VtJqy zP8bVzMQS;C4Lu!DW%woZCC`oQ;NL3IAtQ}^TmgDm2X1y=QCZBMh6;N}(ylLJof+0$AVt`+k|;fic><9Y7_Eh?RUX1w=ews3Xrz(C_y#P&4jXi_Z=) z3blI>7MwQyA2tB&=WiGI6zYCid{fy0(ym~5+n)Q04lUEZX6v^;M~2gtOKg0Ixl4Vk1sjTa82!; z=f!ZpXtXR=`WG8uZ7@feXF#J=SHsI+`>;rmeF;n;oJk?5v;(jmBUe7`ug!^Pj&$I) z#ap%2lT1xWf$m@CcQ- zdEWNi(RY7xZYb7*z-{^6K3_TpOy>Ts=O4X-wx#DX)&i%RXCiI~W;?@_tei2y$UOG| zTL)%K+%8mX>ADpt^S{-6Z)@rXX+3R!YV7->Ln!b(Vd{XCHIzWhd|J{eH>u$SBTLIa z4N!A=W+#17v5mq?6wYgen^ivq-cJ>rMahUFnj?FcvPxgETkM|~pf!%Enm&NSNxa%Mih5A#{@Q%^3Zd931f zA`AZApKTUDlsmo=Ub|qd(KklWUSi6J4>bh)nwssdSV37|7aCsyYax7;mT3VuSoCU?J8m64TV`B?R=$BKP$Xj1IqX0ZnBMbDHKv0F+_g*KO%u3>Fb{3(qWYw zO(dR=(6C{ue!$V;xj-J&A14J&287$A(F2wXA#<9J_!MY%t5=2%kaq{C6T9^ZVccFB zHq3BAChYHkn2BylB39|Q44j41UX8tpEEui(D>W!g-eE`9V!W_DC`I=zwngtAQ5XSER!sx(8$pj){^RhF_v}%%GaGLv0)%tV0IOal@GIh?l7F zKO**D&L9xyJb`MRQ6YK)hIEMmXm{?!*IEvF_?B~cegt0kD3|J>U|bPtA43sdH6wlV zACu4<9S*K=H4_!B+z+`UV~p9e0tZ;SdyE`jH$FHGuPGV|?lSc(a2h6{Dv@aZ{2B`G z(x7<@?rZ1?gLTpXPgi@wJp3DmIN^o@;_Z~4?g-nF+AvHe2U5+--g#8u=iCcy>%m5C zDYvnT`M3mz#9+KOA+G9B3>!dF!$-)_9PQj4a_KOY#3MBI|(cTf&Ek?b;14g-BJ*0|$i zB2k-9T^yk2iazQ+aa#Qcf#>w;uXu`%=%8kI#JmhY4j<;c;6Z;n97wxS1V>nD)(hpx zt!)rS1&6!=UT$0S@>>7BEwcB9P_@xe=u*mp1Jg)hps;6K$XTBk>_AQmVe-UZhhtLsM zHQ%&P)KkZKt$Hd8B^8 za6w`-PLxqiIcPq0g#%dcC@DNK)gnNZey&X%3ij5JHAhdY{E7@w+VS~;zt9p46|_$2 zYJC4S5Nt#+_0CoTK$mCGOxAIKMG7vp$(L>?w2I4DM&hu&|K7?(y^Lzdl zan{}=8>ikH^~DKA2=dLAZ&!|jr^6AB$&>xYFt%kL&e2qzq&c^tZGrs)aYjHc8fGu& zbgh0OJPh!?D8-t5h&oT494=`IXSqGCz5@hSkiS7dd|O(@6lGbfnl)(t1}qp_+VqP; zzG*k%ZE^-!x;t##&zvou5INNFLkG0aZ$oeOdQz)IVYUC>nTfroj@-3f>#Q9r!U=Uz z@K87vTk4CtM|ghwUplfZibt|~-}{5i2)a81Yd~1zcGPBod3RpQgI%ZK=??qMg3Nxj zOVH3a=q7yk0D1-C&LlC_tdWA9{Xo*0MsdGRvBcv8e`Dx77d(J^O@I{fvBxCBFB;&v z?Mr1K8KL_z{~8P8`k;q+ezz4@Bnq7z0A8I&yx~|X*U}!o#bu~rMw%lgBTT!W!HjqS z-lftn+$WK;$u3IaEZoo-&2pJUGU`c9P<1PDI!Vp@LfNUXz8T?@H3!`sr5A~i1vv-8 z)qpL-OdWIq+DS#yTDW3ARJvSG+d;K+2%bO#^j$S~dl9u1xX^?_@&K|Me&osIujc#e zKfnzMz)lM;LOzjdiC6fQ8~(|3sp_R)6<-){xH93)zz)W5=zP~5eJzp)+u`sb{T^gZnq7J``hYFMT2 z?DFSz4u2gI!}FL8d9@iYT96e^STeL+H}@m(t`QytXfYS7lBO-i(2ZvMl_+I;(#Grk z1U1ul#}n+`2o^n?GqT9|w z1imf6-7~2tAm8RU#QD-4sK06V>!6fg?;*Vcz$&fX4_GGI(X=j-$;Ln@eUUJM_U_=| z*-ztF-L>Oihn;5CT*GR(bRdgQ!W$D>!e@dtnaFne_Ie@-ZLM{|QLCX;-IpbkEfqRF z)TAbXl(_rDVlu`AyQcI|3_!xqg#XLrOTdvvA1gE+uGSC}HVzzy?a!Jk{C}jMm6E%0 zNUyvNPx*aCFl5-i0Uf6rST()u>(^<945i+XG~^XMZsY>cv2L{Sk2R_%0#63bp{Fup zwWop3tb1kDG*5^;(f_H^$3nQYSg*I6_JA`q>6lyAv>WKo^BoRyCkCrs#4hpcXjB>1j)Bh z%gLHjQ0ibcJ_X%jhX3B5!+U~BAvI4lm~dlgGwqU9nb?mg>DeGE;kPU%4bg>$B~~%b z;v;hIUQvu#rBC7cAqp)WQfqq*Pia4Kp73H=oWj|pQVQK*!ZdvX=pCu+vQC9`EM8~$ zq0qm!{pz?zNCPGI^rYEP#)RAQq?pkM;txO4taY>Z#f(0sW)91c|I01&wZJ17epo%* zP7zzyw^f)^ zA$Avo0BPOA>G>1*;r~2i^UmD-Om|Wi@PB8`JL?8H^_EwqPGGvx0^`V1Gb5%My3&Qb z#XWB!rDjOl{8VO|1w+jT9m@S{O798fCg{7t>1e)m&8gm?zA3 zTOKtiX-mcC6=9*0ref6DJbm#HL>*qajp}8$)ObjYVd@h&1GGzT1_Vg!)z2apw>ubN z(it?WgRvRDcA&bVFgcIQp$1i?c&@+u8>XqPygIIT6L3ZVQL$Viy17vrjW5zV<{sa$ z#&^7{!3;)FpWlbDgAv`4V2i{79`T+pB)#p6*Vkw0K0+l7ABJF=ei+c2!*@{mE+Dym zv>Z>+9MNYm`YKl5QpUEeCRJpAp0C3H-jfT-G%K{if3FA0@Yxs?3$)+qe&hpelNZ!AktelKEQus!t}U1^P-@^~Nf=dY?{Gy-o^yLkQ427tzT$_^i%`|>ddbz4`k_A~br-=M zb>_)(V4ujY9B%0RngG1MZR#)4;GD7jI^*i@w58lircyw=hbv-46#hm!Gw_gq=zIFb zHY3`pSv1<3V7amY+qO9`j1JkRn@UXmYYN=9U#6G}Pb>5nn9Pe$(!$fhNo>ZuCIb0) zPQhE`{GAk5#hPe`|KGn}j&I^_F|v|2dxsxq_U@b_nlcT>dq9*!Bz#Yd%qFJ^scCPR`AzZ z6^3G*IlLD&rSAQin6rS{){#n*H0c-RN)2BvFx%lt8SQjUgP)dXfGh=VHRmA>{HVwV zr|HelRs|3@(?~#8w6H$&Oje!W+2wIZMaG6T#mjCN__@p}#gO_3w5=t)FACa5Nh;G> z@5nY-!;NVcay1V>pD$2f!~ds$`LWOuR@c0<*g^uDIE%c#%3Ft=&~=3ifum4LPDkv; zzJag~_NRHip|bqcIMDm*E%0(#*k)2UvDp^XPPSI0o8_E$ZH z)n+`xg8*_XUgmGkozm9N=LC0kn9`*RP-kLWI9$ve#>Q)gLo@fBFpyi|=7zi?dJYa5 zBYiNXs+d)-dC!`7$}uvQ|pz=2_fJ!P?3@ zO^{UfCj!C!E)A?UVBk-yh>yZ9Zej>SG@qFF>`k%?o5lc2LHe2v)iA7Funr3q3C$rKbIETo&j7sgNv#YAGi-B%`N`9`SPD8ino5{0@GLi+29l!X zM?woGx_r&PC`OhFaKyG0=$__IHx;01JcR$GV78#Tyl25*%@#HMK(xzA*Jap~MxeVj zr@cRy6Aw@zFT9?h;Fj0mH?8|*&zv54UGP>A!?0JpR!v!S(inns7u<7$WcTT;Ah(^2 zMeDA&z#z%{pZdvBCr^u8M!&7=Vpb`{MtMct&D`aM)0802z`;rZ?;r!ieR8LdY40n6g3P&uoW}Wn6}_7ICXCZ|<5l5K6OP z%2zR0ea!6!54T;SXVoTHhL^*~vTmTcTX6S7VoyG>Ft5Emb_o$o*Mb^DQbpy{W zJ9ly&y4S1QqkrL$kk)E0N}uZGLMjOim+a^s`mqlRz&5m?@GmF`xa;yAs!T`6z`KN7 zf*zVmZhP_Z(eLpmD%Mi#y37xYS)2tlaKm+B=H?0Wqk8&##O?&#isROJ$@Rkc{={6l zl^7w;{u)PN0gT=IQEmlsnbr*vl5R}TTOU2Mk&9N>ywpt$Ba1-6*Z^@wwwe>5tN@B; z7QFwIV6{8*%#@Hp_+188y=kbN|jje`Dad5cV|YlTD$We2*7IZ%rjva}w}m3R=U3$4GyAV_r z?7lW^cDuJTPm)M8&^2@5Tm zX5VlNG*BvVbt`Rr#21flF+LXFe<)A-Pa*woV$&7$;AgbM2H0EQ2(Z$Fg2&A(Z2X|; z*6~QEA@wYmA@~SywuPegarv0!Mv`K1{$m0=H)Igevvz7W@D19TjZm}@fbhgMX?${3 z!jCl?-MpBNS!@;a3P0AOB+A7-57j-d@X!aUUz#^f3J~U)g1Z6ua%l1Rd4GmMax?sy z7*V&>vQ^}HY@^Yz!HxB`gG%ZJVNv3o32C1jP7F#6KD^FU&SD%9zY)(DFT?R<7$T~P zX87#qsjEMwZx&DxtILUZXK?=A%Q#&XEhStMS!p~jB_EU7)_bnO2K+lY#RBP@ck@?K z`g*~97&_&it>&4;g~FW$f2wC5dE!?`n0$qJy$$zWE+eOI(lo|3`IcK@U>?0)U|_U| zEp#(;e>uNzTa?SM_o?-D-IJ=C!-t+dORz zmEc98x-=sOdP2DBexhx5=3bf1Ur+h5O7sz!xyoZ zvch%36lA;`0cvgD;z9_#=Nd%E8!+(2d|YrKu;#gjHw8#=yLh22H9XfM-*c@upx||} zVo)P#rQycTaGL|!&4FH24>B+MVUJDhvJj`0DJDfu1+D^1i9x@G1V@FHmKYlEtWpI2 zz1Ff49L-)+d(wh|IjtX&w5hR|@@q5Cdsexnz62u1az9nirA>Ki$WRbtYw1S*lF-#l zrT>OKN^k*SHV0YCGWbe$z6=>{2**wdE-P^?rEWjcvB5NgJBMFtQ-uC1J7UX3anZT5 zW+Pv#9xC|HG-#f#@fWi0WMAQ+dQlyiL@@p z31ej?>R?*87&*2yr4d%@b`#vwo*rlg@wL5$btZI7<$&sFt;SRi*_DM*5QKy6i9>e$ zR9fBd#=wN>k^zL0UD-k5lJl@ktY^TSLW(=yR@m_Fo@hWjKI0L-VvAn$dbfE#e$ui3 z@>?5h83O7>&vCr?UaBv7_&$)mwpN-6jCfJ?&I_9dD8Dn<;*94&V4EJze^lyP>m5T z2JK1DW3}qXZjoY1t7z~e8g-nhZvyW%_K#1|;Idl^d24V2nxXhx7rw-d4Xt3C>L+CY z^eZ$$|Lwcy0YFluRskdYBZOr`FS*HpR_%IkMtB^y%K|rc&8(N#k6NOE$#R z(*5t)b$=pgz~faeD?S#$*yPr^HTuqI0^&jSX8mTjrZk5?41iu9PejUf?Jc6^vJ#`G zu<~h4pc~>*&LV{^B+i~;=^fI7_+ydWmyA`L9x*Bp;BYxu22rZTmrfm{ah{=0fF*BW zl0GOAIuOv?x|`n$;mJEfMBUSpj@V*gLh)v{4X+W=x>cZvqUkraS^r&FFxpp0eGqCr zIPt3J=!Mp9s`kIz7OsPM$JH*yn!vb%(k%a_PoGA8OgMzoj#k9I%^(0*&gy1xt&}Ui z!nX?k@5oO+7_ZCr;4HXh;XaYW`cxL8F09r6bx2)nKIo15nF6BnU^m&6m2mf1e0zK_ zl>6J~{pSZ1xXv_p5l$=3cs{@T3L@}asVkr$0~i@o+y3cW?pi>So&kXsCd~21f(N`q zL-G>OA)8=mU=;)A_L^5;|BtKvNPmdIutD<nvUbqX=5r2! z8N7~0=(iB@=Ib{1DbN9WaFvV=oI4*Dw8^wK4#Up7`&|ICN*5y~i*@4Q%h(Kf! z>T}z}Q`}GiW?W!WUjXH!HD6Q^fZ*T>(VoXP13>Qnd?Es@=fTeB!oTlOsnT3CY5;VpEF}xP_h>UR14J$>=G8D&K>K~ev515uU`x`#^O5a`jF3y_Wwz?5ctcua~Wx%>V zqiZH#tq|-Rm{Rtwg)Wm0F2kM0rV8J#_v45NmxQoZ64vG`VBVGm}3&F?>vYA6vG$C3n;ReBfca(9F=(EMK5cxWyy}KU@ z_+{Lm8R{Gmeglt(%S+s{h@op(*g=VU@){NJRCtyA5P^W`Ubt8N>+KUZ2T*-WO%H!H zX*R3u=I_M9IKJhFvP2FlJ;HxV4N1biT&B6wP0ipW{4@Z9so0;Jg1i0Hq?wTa^j^adngc~%!mbLF?yn1BlR@}mIYv69dIXrF;xXxa&O zyy2%F!T=#(c1>{c3;7%G{aTo76o*1n99|W$d}n6pFAOYmplzX}Qh1jIvVUzmmXT?~ z`wW{0`5RA2xa*p&Re^tYO6>isfN#dlq5{O6(JzD-$XdB$6C=Ld5hrDxvKvM(Id4{Y zBXZn^jZD#9ZZ+kg!?z6b?NIo@-eJ(XByO>#uf~&bSs?X$dpbWAh$w!4FZJTD`@tJ) zOALqc82k&pOn%*RWJp*hTB3&%3!rXme3=s$h|`I}H>9X8OXv8Ws4(Db7`_5UbP;Ru zt`TM+m)t$bMzD35oo|J50llaNmJ~o z$<0#K_ChmsJqk@gcI8F=?<}*F%QTk%^c2s9f%ePW)jvwQzbEAHrGdHi7P`_Z-sUi- zC)n|BKR#VA`wB89!X4>wj?0S=1-s>r&YvB8wQKl(}a*K(I+g>`9fx;zr;AhoRXbuYu{ffwhpiUKIn ztG#6*e+fG;?~Z!rgWK&7YKF-}pjM7VQuBk`6q(RwGwm)N4&93 z6~Y<7VEYE(^6=03+vwy~A0sRTIlQ-9k|-zy96$jE_#3R_p84OE3`n2hlHhCBw;0Ot znYK`6@{bYNugrs9jP;8Fq~t3p2E#YzHawLpWICG}excd-az<4s;IirIKm7|1%Lf4j z&S{Gp&nq`fyzPH~8o0eB^*@35MDK}*D;V6~O6ee9H5OI1e{mt;UtgjeV#UdsRAVsonpLCO)Cu$l78_su1hlLI??Jy}Z{IDxwj8Dyreu?^aam;LBd` z!oYsZoR{PqK_IpB_0vvnIH-C_92g5y)H$5tzDm zD>cieP67C$+A%c?93Giwm_4Xl&e3G+SVI|e+`_LzsNFhuTapw}6r+-tdHU9lf%!%{5qs|LnlzWzljRcv@GRROxs_?E@k&&3!SvDe&7~ z;oR%}U;34PZGtOtSc5p~nx`&{rinB~BdmiRc*#w;mhOJAf)6X9HIY5+I7ybGk~*6S zy+;Qjv!%^|v@B%4I=_-w^_ow>Qq4B+(-*g|EX6!8{<;R8Mgy<9iL93m#BE(#4OiVM7wLt=}(IjJND? zX1%1k%d=bTF9?jc>{RS7-`_@&>oHdb##=|r)1uV5RCSvEX-w$Z#swTEj09FVBEjL6 zK%19GuaXjVES(+*Wkq>wuQ?jSIuKNQ;~IYE0I1S0q{WX9^Y-EiHW!DB!%_Pj=znjR zKhOd9uF@~**Tus6LJbc#ZxCzo%loEv+42KZ2A#Lh6d@BXAVb_sp$7z=caL!ax`bP# zJ8-B9VCp++yL35hcE>F=4hwkRqKn0{^eSw8livpd=vTdmK> zLCIAzydTXm?0glZdMgadCz5!~@DxCCNJV9}4(DF!Ag}X0ts(sT)0;p1oub(P_-yTfl~3=K-o zdeD7bMJHigRN10(S~6!6bPUDj^58SFcDxMIb`DEAWwe1VxtSP4!z zw_F%b7*ovZCcYJ^Z6$Y{qHkJ3^NfVb2o9YuJ+PqK3q^SIO*-I z<;Zjex&o`zJ2tN{YkGxcl3 ztYD{>tD3c>Ek<2#3|8Ui5S@<3saL`DHQpdo(3S_dqn3Th6bw%Sq~8_j+UHBE2igjU zL^H}vOP_?89|BaUHT*=JOC!BxQG;L1r2U%)8;8}?{X#l!`1weP$?tXo18%*OxR4)Wf{Sx&>T`jDHS5-|_u&``ZxNUp7f9`_#w1Ye|-*g%f$~RcB`ELSr-xyhY`0<#2c6&Ko1r!_r&viznLqi)rLm0S7mk6_jmxL z!qZ`ml!~BxaEFaVxFG0O@+4k{(Dvu?tYHm$rdJZQ+{TX|PAEjjIfxuD0yk{D{sByj zTUxK?7zN-=t)8?{6$t)&=5$H%>s?#Wxx+$+A?VIM;%YZGZP@Ooh0nfByrm5!<$zTo zsr()qKw$F4*0c5z0w?D3{sF+vA;4XSJgZvpj@M6CnE8%+sgx~!@$cmgSDfhSd%}k` zA#rUZ+!VNNat=Ny95dUKFWdAMpRI74HGP`8lodI-XWGpzPm0kVLjZy6;I zs5HmLTD=&d!qHcKIo>lIy7lxkZ1Uj#iX*^(&(j3x0j(PTGI%+x7nqV2_fahmhi%<} z-hoD)P&sA<0G}^nt0hA95>Ud{;69VBK33h4D8LoZq0gJ-l-y=TqMm zPl~K?^c6e8Hx2>m=2DL08l2zG<5njXE$Dk_aDn`mLKow9V`4;ta@~OMgl7CgkdOwj z$&@xMfpWIzzJ=Pbz@3r8&xFRU-`?hB(M|!|0^-|vU94k8Z`5_z{#)2U_SO3FH5*3O z>2oMy6^x%JUEFYBndHLNWF3E#;Lby~eJ zS`=V6>hkSYdKaoAuv5)kL*Ae)fu+#Q+b~Vf)CXVuHs8pM5ns|N=pd|tldRv1m*JV;r>IBAJXX#XXydW9`*3ZEr#}OB&3y`R1jeV+(74@ zlxV~>suLCfHN2KUb?c=GRz|lTCfoB|8$+WsZ=qpQcd8cLA43gv-Z9g?MHLImA6X^* z*alr<=A>7&fB;A^!xaI!%zEJ-$hccUg7EbL*rhoxgQWfmE2FRy?l3K#vS=}{OsvO3 zIDdrmmd;~S_uk^if(@MB9s2JlX`vb9>H&rZn0InY23I`f(==2A}Baps`-h> zS{|@z)q-;Jv6aqdEn~rNEQ4b>Lx5Ybap-432=1%@7=Y~7o*KG|y&CvN zRpd;+LjuHJ$q4=L)Ec!%I+gr?gXdD~`l5k`McD==tQ<$&NIwln4dlV`F^r&hbEGGkR~Qi)V5{2)>rMHu`y5PNHI52z=@MgP7Lex|u-mWzLCH^$ctpCF8S*IbwF z#at0N#5%NY@#yV!60bHOnug`K&u-DhDX0Z=A#LZ-v5EHobbIbE4Ri9C84}WZc`mda5C_jT3T$Pzq6Z zuFx6{8yT&{Uy_VpJ;NrUNN!c7?#s_((`a?ioDr*z)E3!d5^G3BXvk*^?jZAdC(Ln! z!`D6L;feiLA?axlVC`m(ynWupvJC>p5Zuwg>Mg;6;W=g!7V3BibsDT26p?sgAXYOy z8Xie+0CIO~Dk#SYhA*}FxpD)eccrQz4JOwXdV8IF1EDt%QJzfTiUYmIEgH5Bx1vLB zQ#4+rdJcbsc|&k;J8Y! zB43V@Cqohui5yJ3n_{>`C7*mN^$cP~|Dr6bYuBrV_9p-%(AAm|cxx(guj=uxsHn%0 z+A`bboA_I%f@bLe58Fa)>JMaif9U!By2 zTMFQRUFuCn{9h%{JZcDKQ*HdqT%($bJJjt9#P2*0l|GJ6H5Qw2ZrF=23f^L2lN6v2 z(0I$<7tWEI(hD0eECkvTGRU4)4mZf-T6jqn&#lpX=&%q1)ndors_#t+v(J5c?VMv-Oq~}Al()uE&XX~UJ=O}Z_j|?d%q+L{Jl3iROtbFfh%uoDYt{w z#!Z{I9aq1!62w16J!a{v-Bo*$Zx|$l9pIgFT zxKf9AL#t_aFMPw)#o5>I9d5i5*Yfi0v@b%M$U?(_zyejmx zsg;*2sMtH)cw6l^i}7t_mGH`t(dp=*H+75L3|?=Ya1`aAoLS*10_nM19WiW5H+h@q z6ROo#!mR-5GBBH7dKoR?haWO_wOuV%H4FzOpdq-L_ z9sC%Pgcl2Dym3gh&IEcrpM&8+hdo1iO)wq86@l=`^GZ9AT#1G~U#zYC5>QPi^3A4KJ|jux6m1SAy+BNsG3Sy+Rke+Qr(8kOUpLBi$T& zyLjb^P8ddaz_|g~V}`Gldo%Cv{KjgsixLY;c+C~vMIiihzqi=rvci?%o;fKGG8I6o zK24;1gRWY}Tc=INQgwy2{GL(O0q0Txt@*TVM{0T4-jKRRl)Fc1O`&eW0cGe2UejwP zGIe=ZIQ~$CuFX1gZ=?>RB4(c}*um0mx$v;S#tHC}zn|UOqjLER8DpOio!SBDY8F8P z+jSebuj&ss9HHmi<(W7|JjYw7zuKDxgkh3`OeH|PFa>yvI@xrHk#>2j=kVH{Zi2n9FTjht6s`4)!_U(HZ@PH7g5W5 zhtV)rV!>G4e_z0p(z%jkQ6av$dFe(sPHxFLVp<);wbX-OeOjJP2DgdtuNlLIMC@)B z4da$o*zx5A-=2o3zApI6AQ0 z=$XEsd{rYURG*;Q7tB2Nc#Tb$M-92N|AlbhEx8B+4nde6kFb)Fq?og(rou9MK86NZ z;oQ1ryX4t^d>lAz31-AwGYK2*??)8k6gzx)NA3o;l|rTSqHsZw{U^Oz_Xq|&w4L?e z9pcq?9%sp)`@$iiu!EJWM7Gx*jFHyOjl-X|kE!KBGa~Q;YpfMv!W-By^bAl{Wx|u~ z)q&-%%NJ3VSExgB?;+NauNF|RL%Yak|Kdr#9i9wv{`NHgD`r&^dvMTm*V8KqMo;?3 zH9xQGfN;mk1yk5@)oLb`MZj=NX%~`uo7zQp zGVE)7_8UH~Y5utgC81&ZD>U5eJ%Hl|=O23JpO8G$jX~cgBOsgerLn zzyL>=b7Nbyrphl3t?59{+H4o|w1R$}0s(yh+^soDw0^1GtA)Ed@fns2|E|C_Ca_Bf zff_}*4gOSvI6V?~Sh>HpIGJaNWduh*odMQvr!1ETnssegY6%Mw%6Fkc_D`4u1+)JF zJBA&URO_6tbls)MF9gxl!{BhtZ*%;>&v@?!(Y3Eov??e0Pbb47nepGGe=w7!7$2v)hx(}3K(V& zrv#GM#J?-CQfY9D*uEke=EFekGTdqwbJBgiJ*v$Y*Uj*!o3p(C0>RUz(Jv`5%JmS! zU-+$fQ5s#`tn1dTbb8srs2Dc(+<;fxPi-oeue$~ecx&4iI|gxz(we)6zY-DVlrzIs zCXFmK&$Sw`;H69QjFQN}fNtT}n&Rlqw0VBwF;_TBn+XHzC8M1|PF)_o9E$E%p1Goc z0Sa@Wa6v>2`;qYzE;T=Ubx&{$jRsRLq*kKOZs>0)2)p&-oChZ)rS68u4{$fYKF2{y z2$Edkx8k)anM1COKCD{rC0x*KR@=n_Qz1XuY<9S$woj#=(I6GsI>8x!m=$kHRnj79 z15qw))*W&qf3mdGy<8hL4_plRFen=#L4dlFQG~@33T_Z~157)g=mT4ZS`7;;Vu{CV zVcEPxv92TQ0#eR3fNoEX;1%$mvk?m znxp~iI?v7GCq7?Lq6a1TK=v9@xws7%?XhX}a5qlRLw<3~)UnJ6U!bDCwY^f4>ErUV z!+{IDH{cvcmY^U`7&klP`k{Dl4ZaiyBG(WF!0d@4W75~|GF2tEZWQIv>%xm+j?4~B zJWG+cM`9E0ek{cG+)^%jsUIqmf5D~!d33QWFd2FcvyURv%M zmiduoTuIUd89pd}17I-kdU?mi`aplCm(-38pP;oap^fr4YQ)-fv)Q3wu$mcUB?B1G z^I=Ui!0RnL7s#qTxAwg?)a)Sk)>(4LFB5>q1nU8R-ksa(Lv$cpiMs z^u7kP1(@(iWfcs|HiW)x%C3+)zxfkqDi&NrO4_4ZRNEk>u7?=o3AYB7p}j?(bo5+7uRlTR zU0$#Mctw?XpxA7f$xOI4gvg;s)xOobRMljjXgJ~5KrQ1R;15(R(bqQT;?myh($mIe((^JM&IG-1q3J57IpZ;*1Zq+F!mN~ozv z&l3PecA~lSzP6dbf_ddm;0k>yXEd0KOtK8f)NKl%p@I>f&@dGYuH7Sa&q${VZp%@Qf z@MH|Rz109ehuj?ryd3oHli?3T`@)L*;0qocoZL=Xc6s8Fh=HtuZ7{(AzU0Rt%MD&$ z9%i2~acDCeBNm7@l^W@?r6(*LHkW&}rtJPDJP;gFE0@;K4~vuH4>bdaor5njJ_h;2 z-_;6hyOj@Uit6%pBII$62?vM3IXI5fY7Iy8#m4*?_3m72stJo~q2u2=#fLeB!JWQP z&|COab~kOgJpG+4j2Z5DiCt_r!(?M7WoENy%HLsq-47*---XtFQ+=T&SH^ zxF?Xe&8Ka&>QS+Og=)(nU`~-FQnO8&7=~PlY1wQ$J@r%#Cok6at>9Dym>1uSeG0-1 zc$Bb`4!i#SWNlc)cgS>`h5!p#`tqJsw#f8QfS7$lzYtCfNZJ&Dpa}$c7rx|}RM=qf z_M@~3FgQ3(AC~qVv|4Uq3k2(zXTQ1@XvtG3+!?q}y$(n8jgj{`)Pe(8UV9sWPaQKT zi71q;fPux&@a-JY#2YAjhjXi)NM$RpU$-bb!ITK)=7jg5;8SN;tN{z^u0vrBP+xiv z))7Y*C?$Qr0Kk`4N?BKO1S8PUKVlGmw{ME}>G7fOM$ouV0KPMqe91Bxo^bIX^912H zj2KS)NrW8n2wMplzvL(TkBwXbCglWF&&Zfy{FYatf+kW{_3rgdo@ps;l#9)^;ILRj zw{XE|sd$!px9p6I@GT5#Ik{BG)N#A1oF`Mp=* z>c*6}!&7Kzi`l|oXXq^FaRM4btM`RH7hnON4Jld&c=Z`Ud7HGw{Zw2$@d$a>Gz5>s zj~!>A$`cWm-Cc&3epI%MvioN9FUYKUm+l#!0u&{Cy^6@R_tVZhJn{{nrIhR?+@u|ZRTkNL!H!>A?2SQIJ-4h+$v39r)xdD+@A!soO`6iTXfSNu~|T7sb$te za&roDZ)aGy;B=ACS2aZh66mq~tWh;&F0m6-824m2}Iszs^L?ir6Luk`uR{TGTNk=`HvS7#1@>&!AeCpsHyxkTs z+7f7B^&HI=Tr;c$m?D6{+h|=DXlx4|BIAXR zJXjhnMP`XYp7wKy+VJ7EaDa8Vy`KVHpV+q|OyrPzv7P!yP&!WNrS`jd}QeT*wD06-;I2@GJOEFriPei|H)23z)WriQP z;Eb2w6UhxlLUHV6fz$5y&@DMGLAafvk40SCqD$V4paFn1{onO>%;$_5!3gKmzG_V* zOV8X2SY39pKl?XY0b6BFrD1U0QeRhSiOgkp^t5jvoFUtv=dfoeK~3}gf-3CxYx5+e z1+=z|49OQ2WCifHtG^<0jdr;>Yls$789t%mn6M#h?>WJioK=O*>8=I>!c4h6O(*jr zzzVM>pxQtLJ^Dbbz$`a`ZLjyG?Dv{5}S621(?7 zi;NM6V=;COI~YtEHuJ^fu^?bAs6v;Kaj#pL3cKZV_3ZiuHZJEmCwdZ9SjmPz58--! z95cfDu`CYffx@&m!5sn!5($N{ar*x zS|o5h`PUW*#?#Lu*ZvI$ll@#3>jnH<|JQ9ecz3XcCG{1E=WLA>38k^UsXHw16SnZO2c+qqnF4v-QwJ%X{pyrq?r z%Rz!xw}}8IZG@$jo#j4y&*aY>Q zpE$q7UGaFJ-%2q3>zi3jQGio3?&CexlBJJSU#WJ682z>IGhz8Y*X#UR{Kj#p1YQz8ddh2~Go z4Io#7OWu;)i={{e_TvVUD;?NdR!9T4Z~5bVgKrxxYs;AY#8nem z?A!0UQbk<8SjI*jo3_mu3*|2O`eG}3%wB-9&1!et)IcU)`pETiQYV}g#p>973_ zKhVmR(^I^gkwNo=&8@gChr4$;K37DC1<+{2ST`J#`(+v}37Fw?1Ax2Ne6=>N+d}HR zQThg>w#?g)>}-aKVLz#$NqR2sJX6Z_3n(5(*cAs*$v2{*6Gw5h^RUbYb{kb3N2LjV zQ7s-!+i+a?f5NUTNtRSsZm7)~#C(_X_P>yqxGS@yAVmKkPGrRZlFNtML)4y5GDJ4* z7f^Tr&2?6qL~%;7Mudw#3y#5m48g0$3|eO8(onQKFVS?uu)rkl0g@o;XpYY~4o!0C z*57m$bsS4Um4hfn$xmB{Ma;IO(2#J~M;xC!rru&S8pZ@~M*by}^zX2vMN(~XfeQ6G zTj1xKn{^Id^jYY$T}-vQ`qNsxVga+wqt?rLr>q*sVp|MIn}_2fJOZ&ntm!V)`98l1 zwaV8M$&-^8jTKsmP;GKRQ%aNDz38hjV-NrR32;_cmb;IO_Dv;7Q{MI%zya)cz%Vp7 z*_me-N`%Cn9Q0S{WnMTI<*S>phOetJz{u$)p4-YU1kf$pDa4|~0?L3xCtrUqe=bOB z5w7*6%~MyjC8+qF#r;ky%1V!oAk@>pxguKOT9K%~Xr!{0E#5azVsO&AZV@<1yKJ|! z5VoBu3=dB7HF)gPEo4zU+r_tQGq1r6v5?vVa0{cU0Aj{?9NwOG7Qcwwc8ga%Wr#}J zbHPH3$!NK}vLFQVT#P)@a95*=?#p)$7oCkz*|v9)QQJc655{tW-``yOL#TGMJpO$W z*E{QNm$SGC(5@`;;IFFTj^AYo(YldE;{K-O|dR7sZVopTMLa9MvIANeBeN&?aXSCVw={hA_oZ-DA$u!;xF~n zt@F}cBzni@VzqT^FhkGbeouD7h9?uq@a~7Iup_kh+kYapV-A>JbXCZ)=cPbO0t%7~ zIama4QwwuV1P-0FzH{d+NwyMN_hdNfV4-uKU9?rm>9vsf0c_sRanVv?f4gUKTrm!U zrH%d5h}p`FCTp8@xo}UKyAo$Xwvl-zpch3uru(~y+Pct|r<+EhR6KY0Nf5hrRp)xn z4%O|)1tPDhv!F}pY$hQ!OLBiB>)bM4N#{~kfa1O)<<`alsR=Grg{Cy_XOfr@0+mc> z+HbY{j%cHR6Z6d*yk%_Gz8@mfwn)0TL~lLZ+(z~GV)RddOy;$m{>yBhcD}txw{4}x zj_hC8qi(re3bboLQiOJ&ip6(h+y&p!i9&`NV@e9^K$~V#|!8YO}`E^ zu#Mri5GKh#sd%w3HU{|{5TB7_FcT3GsrxAnet%z11aaNzdFYa}MyFadFQ-HOv4Ycppw#{WEt9iWpR4%MH9++|X}5O2QnYa4Ez6Hwezt-seXw`pI)JFyG- z=4`C&tyl)qxj+6X?uT;nyjtA77p8t^%jnFBwz}I?`8-_KFLKTr!sSE;Zi_wn5OzQu z%HSuTg_Mzn(a&2aSJ6w`*6OoP4st_2bzQsS8rl6&Rg%T@OokpjTkxEekDJ=1gV^dd ze2mTgjg5u|ZxJfsoifj44Et&W&c&!TjtNaj7006~#%J}Mw`0;HvR%A(8$~=8$H?RJM9hn_S0fcYw zzzM1rU`GnkXH$b^jNVBp2!}n2W&}b^j!Q|IgTvHkU8HH?mtucI6rex8?IW z0P*Zb>zEU55%LN{lNkB|cOsjEo>L?D$+Zsk)Y#CtXV^<##)y!!fom|T{oEOc=148$ zj2Y&9+--R@NGK&DDKN=*BMa4iMD#~U^Qlo9Km$P$PWF`~j$557`PmMsi*xS&7jj0_ zV&?&fa`ty_Eo?pu8JxHB>$DSE2~4={o7~p>Y*cx1;+hIZpw+rAlevu`$zv?>S;-8V z{2xK&P~xO)?|WwseT{Ns^-g5}xzHA2yV$*IuPSkwvFxqrq-Kv;ES(VSvc1K{{X}); zNf&R5od7y-U&@p(ilyHpHLDP9PAKG9^|6>rERb<{y2@clH%wpJ~eO(44qK8D3 zpq;gcL&vqP#BN&;&tt@^Gy2!x6#{Y*Lyi`1`?YTFV<&LC9oF7Ru$8nWs}9d;DwTh| z%;tc{z?RHa8H8;|@0UoktXs;mKW{REvrBA+wVkD5FKPUf5wQc!W0+qH;<#8TOR*iT@R>*NgW>dE2TU)*vVHIvZLV!gLJFo^^ZZuf>s7UjITKIlZsBLHOH zW(l_jB&c%CGhF31IYjK?NGd~9M!3oCfF$9%TxyZiPlI9bb{*dvYiM>HuPw8)YsJ1e zUkdK4?f`snHTHLZApl&+YF>j*kaACEqpRB(gog0XmvgUw(+5fxThKBN4Kb4M7u(}K z!~^DT-t5JVrPSC(Xd`!EbB$>ij8=Q|DeE|{;5~+-53TfUZ(^nqOB?ZtiK!q4W^Q3V-mnv} zFACV(Ga@qu&DjIiGZ_V7!f@+&D8=D&0!`XJBW!AU&Y4lav&XIIzB!O;+6^?*__j-! zKS27ydQg+${Q?GO>|<2FLFpxDcggX?Xr3<$wJLCfF#jHfi<`FPCm^s3WQcKw{qE?b zAQoJHx;r*{`FC5uWFodMdFi2f!1LRfP9OHBh{{)6%=9WW%t8fj7Lu>s=S4r4>4@jT z5_2YWTU(1`f;7^-+x{{WxJ|FY>{OqiQEzTZ#yc14>mb*E63A9vzef- z5A%Hj-L23i*&*GTMBQ2YMH{j;m6Rmg8%o#PT^D(H^!z@Zx_OVgL5ZENj>|_YgSS)1mc4h*G*6J?C<4D3pI2=ci{=4Id zj<@|9X;28^4_6T~^{jr?)t~DVHk@x@L>7i{O}b$!N+OrNB=gT!C=@B*xjg(L%_TVmND(D}zuycUmFzZ`P9yCe8nAA3i-{z| zonk}B&-G$qqp3N|ed90`idHeXO1l);m*_UT%V)TWynb;RV zYhxDLZKj`~7<@FEtCz^qg@@P^1{G+F3&UO9z3fK~`ka75!{o9AawftHNHZ7=Zz@K8 z_=iD|idOt3l2~Y}0O^J+%Lj{=889Fd0IdXGsH@A!%uTThbJrcBAwq7uo{Y*H=>jrU z=0J(qVP4XfGm!zvYqH;#+~gxCXzxE^vR>xH;!E0o$d7QkX4oP;1HZ8KXaLg4ZcZ*I3o-j zBopRcBc7XQ4Cs-b?Z6HCEEI_#hm47IVNkHUE$vh~Jp0a_cS((_efq?EB@T1jLzltI zHMuhsfbWBmdLA!8LVIzSWRD~ge;ZAsA&P@g*Pc+nJ@)=}^|poySqSpB8Iff6tHOP4 zi{;EZzAgJ`cuf)6JtG92&0USOxgM5<1C z&J~p&LDd+HW+BR3EUtCH`<-@fwzL;gbbFH5J~Ppq=COnWfi>O@9q8Z!KN7P>8I~ii z%us%yh_rW~%-lM1oK&+5Y`7HL>RJf#y1nd41{Ly8>$EqL2=8`o`u8brNT|npc9+q( znEZZL?qWP4Sl!)0P629&V4&GxIF!4K^Wf6UGm)%*XJeM?B7}B(z5zp-;_K}n@OL|e zl&BT~io4I86!sPS+~|>8n#9&Et&#_qll{bnwExmCAF5EMCBx!EA~X=25&4v6#mt+r zj}Xvq5!X~bK^mdAt97f$!Wgq$>cxPi3MoO&-Bi3Z$wBl_^TlscdY$gK4k`m9--InG zRDw^n5SGv_*R^p^g6;MxL8!Owse}M;wW8|7scz~gZ>I@DX70K~oa(&Q%FudtERd8I_aPT0 z)P+!F25VUmz%|_8*jWzF@MT375LiAAZx>Q_YdlpYWp|%|%|8z^VMgeDyZZs{BP?l3 z%K8PZM@cGbs$;#(vJl`+4##{F9Jdj1SBr_i5390UO6_LY&rf;Ufzsz=`R84R(TZkU zSnyM5yx?%H{LCTT=7BqH-7?UGu&y@-dCq@R`-a20OL9(K%3;m2txvI!x)OrU3$v|T zP2QQWZC(0dMFsNNyJ1sPZx8FZnWYl2+sE`9>4w3B-~ie!qB2irwVP^+iQL)hR-qJw z`9hRgo8bss;V@*Gb)rPweGtCY)fnFjn3$uP7psxhU2oPBg~njkLMDqEFO2+}>9_H( z&9+@)uT}ZI#gd{emQmeZZ`!wE-YC$x~P)*9e-SyX0~b5FGJtF zt7F7!Sf!+pvzunv+kmOA-M8h2NY~Ji94CY|C7%yh6woIZq=Yw$sc_aH$@Zi|;_luv z0A$dVJ4_LRy8B+8g5m?L8B!N;_B<`Y!D5_%+R@E2TKuErmd?BgsGa=RFwS z(tEdQ62{#h?wc>Xl2U;Im^D-><`o-g3v;uJxb2deW;6EnK*TRUI@cEsX0AFv(kE`Y znWpV{=!5L;F)jw9qT?{JxTOp=>IhN%4YH|+{mSm1Qd}f0#C3~klVeGBLR_~xbauRW z745&$ZM%#$%X3}WT)W2ITpM?uO!={aUj3i9cn~|M9q`IzB*eIR-wW%Nl3iiMOtTX6 z`xCskFaxdb4Ir?aT?hEYpR2aV-4#hhc2_)&**jrVE3IyiPWmsvoBoZZBPu7z{vk3{ zzRXi?S4mp3OduhWTVpA}22DgQH0?tGl3Qj^mq^;gPXeYfW@ToM=ZJCHq@vO4U>5~+ zbJ)BUkTZ4)l0{dAfNsxWgUVo8pk>`5bgLeuSV=Dl=1e582j|y)p*+wxVqBDhQr%g-w1|lDbGNV_DTDL00Y4%sb4i5joE`O%TheJq?U;at^xaxkJbKuh>7LxWUy65Tt$fQk zO86L;*wS_TX5cg%N0Fwb-*kEj^3de~Yqsdx}S6Ti{t930Jug;+0GLi1S|L{3G$!xWMP} zq)GBn6W4bZ(s zE0-J^SG(;O5_(5kF{+)J(>p8?xX^;ZMa><1_m=kOhpe?| zFj|XcR|DESR4ML?%;PNmVp6?`pY9Mug4ufmi6$Y5jEAFP!@)cgT>$D?OyDZihmY`NPifidyT&^v}%~5zcQN zPrVe@G#dHNsRUjzVy^v`aP67;#;!lU^>2LW7>*_2I!L2Y;a(#T5E5 zI0pOE%y7fr3C5sy2m2c9w#(l4`KgHgYL+cki1WU}Z~JIY`fj?5k-jiPmm{PcF(KGh z)@>f~8!sYb`criGBdM~N_Uc@qsZrhXoCz8Mj0>9P9Jvj! z%PU?0*B~}-eYu~gDz$XcY}Tri7>g2>uJgs{OJFSyXA6gv7bWdlLe`66I<~Osz9GQ7 zpZ{!J&XC;|rSp>G;0YfTN2SSt24_~zA6GQ=p2+mFE5(&nr0s%c|LSeL8FicBVFzht z5VMnZAq-HL)an*e zHT^c1%%HM1#M!$a$?)(JhP+YM1_45LPXWj-f+Rx0S7sJ&ab9v|G920t=;b-#6`p%C zyIZLZ^i%x-k%@XJ+uea)y6zM?)P>(*MOQnJs=MViZx}LCrf5dv8Z`|;OK+^;4S0wq z4NbR*`$-Cb^^ADSJqvRT-9j8!ip1H+P2bAp@BXC5E6f-pAdh*uqhu7ox~YtO$@isH zWjm$(mFr6>myy9phUE@kO;;>_)6m}9dJ>oG6Ll;!(k-rWeK|;gJlkD}KDx;nO_Z~W z2A@>yEUggEb+K}%W{%)Ywy`3>fuJ@B1F;gO-}uZex1x}ro8BhoF!BTX(A}0oVy;iP zPv^)ea|mZQ2xp>uBuLZ3x>b8aC_|f(``K34RwA6)_E6fNy+8x z^Lg6P-*?YF?JHG@+d5l`Cpk9LZQRB#eAW@|UpejzDkC&kcI&7q+J#W?ahIVkkrsBZ ziB^^~B*A2O-tM}lP4@><-kNgEM3Sa)cb1+~HSlcRu%bQK#hGuNXe&_LQk-MT$&Y|V zi_?r&5^~eJSP8uO+x`EtjA3sUD zI|c8}WQlSu8o|4JL%z$1WGM+!LYTNZWpt$jLvQ^8(If&NdD1;dOhJ_^r$Dc+yETeaIzs zcY_!R&~7uWg-WJ4Ib0=(TS7`PsSj{acHc5H(jG2CtC##Ce~PTwp)OMm=MSvz!sgf(m-r zkya(Xn|qAtPtd~gYc1ASX~(d_Sb)t$VuF)2_x%IlO>K^Qi8L5BuP-1Cz@5wA@3EGozWI53tyBC6%|8C@)^pHw?Pu?0zXH(UrK)Aoy zqV~FOHFlLS@2auW9+VSEyfCu)3Y1q|&h^nM=ArK477^VU$QM>fTbfQ~pBk-q%Zy_= z5Dq|Jso^fBBcy)mYPOVtxej(=qtrK+9??1!w##mhp#$$M^Kxt}zEIsC$@6KXOG+t6 zxZcjA0`N_x#agHGOXVK!Z5!lGt=NiiZ!mNMar4+pGu|;HOrvZx4}YTNAXd#$EA1kO z*hdU3yDVZ^N$AZh)H{MdOczmc-8=E$65Gepasd#7Gf=AlfTgCe&7|=xPEzO0S|^2T zEBN{+@@8p1Y74hX10iR;_OwZn^6eMgPo-Cb^w>bF5pOF_hfx`*w$FiAFHq8lohM5r zvo~G9W5{dKw_BP=r5&%^wU69@#Ms4V#m03p)H6=3-6c)oJU3eoQzf(amb{&SFTv^0 z8e7-Jab&$YQEtgUXaMg15DadvT+C0Cc;*c|Gm5%6wsscDN4uESU`;s1!M-prgL*B7?LB$C`? zTh4v5-~?QD-TK9YykeIH4+@Mmv)eXc!BFHxWh4Z~WTx1&x~WIQNHGI+tEULi**n7o zwUk?2Dk~%?%l}qpk+1enZ{AZ@DA#P&+L)d;dz_n?p)JlB*V#8q8N3pP7VIBN&tRuR zYnk>hDfVPVrep>8*L^ja0_wCbvSeY%u_vjwtR9)`;&rJIOUdp70wC;-il1DxtX{s! zEfhxBhtH7gt7Zb`oxLR%< zO?h*eE!lBfVOpBqLe3Iqevj7`jswtDnMIssyBOg0B8mZwU=Pkl*9J7MfkoNZv27%G zMGb3Q;rHLZ92Y0qZ-j(r9Fk1cRz(S9H>#o(&A2iw-z?~6b)k(+-jQ2u2bwfOSR}+r0jkw!zf&@ ze0=eYlF7|qlQm7x7d1ptON!dvJ*6H$_8VqM4mU*m!jR$HW1DJD)p38O9p<*ih|kzj zeamPF2UUyYHFe}&Q9-mJXEjE5^$;n(j;nB_k+wv2l8cOLM1YfnOWKnGpkJVyA+$!( zb&bdQRXO3+ZHNGYcSz-Bo?GY*Ba+>19h~Qd;-reQUzD!w%&9k@P; zoL}d1{gf>fC5-L{?(ZBW<+ogpm;~+sFfThiZsh!4$YB*$iu=fS1##DcI0q&dbd4(x zerRgZTVK!>O^{Ac(OsiE?-szm;?4b=5djq2EJH2hL(2~`GkHeRT*1cZwKc}2N7&AW zI6Vzq)dto1IbF)R zUcyj?YJzR$^!IMk%3;6g|DK7O@P)Jl^hT(+rCZcgUFuQS-AYcfXXtw=Xy?*1O`tJ= z4Sr#7p7qgNZOOFIww0eXp(`sUxIytl`kd^_gj9;^o z+(x|D(Vw1NVPON|Xb6p*Uj$pdhp2VU=5+h!;-+Guj(q5oIBTzVe$tjgGNBp%{0a$* zQ=K-t^7bAh9pxKVl$~{Sqb)CluE)8sVE0j;nP!dt4KB>*Fic|Fcy)W<8i(0Hds4g9 z($MOf8W3I|f|s8~wq{q0^Hgh!@wz$k;Q(B*{Z-qKcanc&b8Pkl6%0+QdFY=Xe$jAq zCvr9eoh0C7i>v2d(XiQSpF(HoT95tWa{30GOR~66CkBK5G_{5#d9;! z|K3RvJMrF)pr0>TLD&~cf45|u{J45%!=a?={;{cL!z&d-+K@-T44}4w^RY*CGJsppvDIShx_8Oz<rM)=lRoEXDN_n5lEoHJogr+_rShQ0s&}enw~#wd9YOpvbhk+Ewtu36 zy**vrgo_4KY?SZsQP91?34QYuXWU#|I5Aj!tJ9?yVM4Ds3}jl!OG(MC;~CXcsOcAV zj&z=yy`04H{1k9*3!U;*RrGSYeH`?Yta-rxDQXH^YzjQT-dQENgP7!B3B^X4Kpa95 zEdjKX{kuZ#009m<+FC1!PDh6soBJ{rZ%+z_Tc?_X_&mn+4Jb}>x3zM7y~0{*g2M@} zy6vtzS-?JX>~N*hgc)ZO)QJGYX!gZ1WCv}1UGK~q3iVsLxXUDFsEq6u(lD{caS8~u z0X1v+J43_7T5r+S>Z#f-Grp7m+ubgA_-7Sr^&ITqHI6Oa`XJ4zcd3Y-{ti86@oEw= zj0~nRYVYL#))b9j6D)>OIrde0__kR6#dNeM&u}?O&H3aE|eXC3LlIL)lx(})Tiq!6xP%hiB zN6Bv6?7L0W*Xz`ulf~L`Mdx_FEt2C}jp>x-=E$9V-=0Flt8LCv=s@QDBRkD|OF+0!V5V&sKs(*(gKvm! zMRGfx(lWwCEh^ms_CC^UpQC}rm(tBz`k*MK^^0?)mWRu|TMT3CDfVpUJ>7@A6$f-e zz2lNoU*LAP%U{=wXeQg0Dx-8R1h|=%*j=16-;s9l|!$G-2>D8*Lyv5 zy7;zBel_N1n0z}Ce`h)yCg0E2H!GNJxqcm}uVm+s`D~giGcn-wX;!j`k$efM$l5g9 z@k!TQwml&@EY=V`ykF7k4uolK+$XD^y_;_T@)1o)7s)kne-#Z_d$?b6 zPKFe(>RKMsdx71)CRQOK>*zgb^03@M?@4Y_FC;%`>4bboZ`S}Lcp7bf$X^Me-o5E! zI=A-;2yEPgquGPgpyg*%w;u;+LvDuw?h9M^w@1Dq2aFZ$sFRtW^KF7iE#VxHU0WBlXuz=vcInBj zq~^r(_1Z}uhF-{p6c6IOu5Ng0PE?9pB)f%did-1_vwx!wL@e!UB&9W!NG~K14blVC zUXLDkecdu9)KYrd%68DHHS|@ zV&8$0#>vDvZG6!CyKQp-`W0WSJ{^g7l|`$e`Dq$WGMqjLTCpRH|$7ukm`G}LESG8$5_5E9+`IXEt0D8!&2d|(YOve_TJzo z3MBwI+Tv$qzC7hZ^-R?NFFR-$1b+AHbnb-^@MHh9hxEua3#o6Wosj=9BG-aUgACu& zLu?`g_2V#VxO+*T#5i9$Lt`%V!?N#-a0(fB(wRnP5RbVxMpWbW<}EY#eavV~-NpE_ z^b~B{NfXq!7Uu7^Fp>>PPH&AHL_ccn>n=uvs?=kMNJSscf_ex?)+L$)o+jlhq~29Tn*auu1l-}w9J{gdhVw#Hns8oi!gR#HkP6= zibkBrvU{Yo?wMP&J?1DJ69nxyp^RM|H+N5qimt@3=s0*hbHT`U$g8>h)Xz)pkTgXY|h#e&1 z41iL`a%LYJ{H!$$Iq>STM-%b2SFN4VKWwrv9W^VNw>3uW8J9RXx8u^$NRV0SLhw}8uGc~irn9|% zQhOU^rz@@#9OU)(lrBKi_ORS#nw_Tey<5ZcR@CWithh1!=yd;(d`1B?z%4tNa{$BrJm3-dYT^`8 z8_&Njy0`xjhwU2z$18=8TqJ|qBRuEog?(QMpU$&XbhV4x?F&K zgdEe(Og1R+_w3ZGx82;DvX1Ork3su}QR^ixLj&2e9fgeXlB)HTCu3@d4$3}=L8WXS zm-aHLuf0sSa?tqUEmey>J1Eq{Rhw$wAQ8C|US>QYefvcmcY74PseAS;W8W250L*l_ z!{z6}u^-?e(io)edaJ&JE>}3H^Jf(ayemZ<@%(!ZFp)G1m6e&J52^0v88E(B+up$_ zwu_*Id5C=5+=i~!y}ewxS}m!MnFBM(BVCjIfkay@ zDeoDak<{8-?3OfX(@;aU!lrJkgJ`j^^-N?)g^pYq<087=*ur}zjiZQKHpF&O$C)j8 zd(z1_)gCL=;Cuq@Sh9C?{)e<96PsU8sm4s4MweH?%YIcdbJV64NoL-2Wm6mD_JJv=iulQMsaX7=u97PfEROACfyE{0+@i=ePL$qD7y*_ zMB%&70{3pXJHtGrRxcib(v@XmYw<|{*Ow4aTbx4svGYhZEi6>@ak5w zR9RO_S|+m6Ez4^H#VxIscto2t1=OV*OwmM&e({*ANjWDL>zY7uKNV}Jea~O9MRx3~ zS<*jo&_HI(07br8lF6D+7fGpf^-Z)D=LCccOU(CAV!&7spp?JS&5&Bm@q>y8y@Q)&pVJqra$CH55tl~I@k z?EX%=_4X0jsO{T>s-U?0O2OJL1`k~TEb{?MxOw|z=vuPR;`ou9P>4WJ+qq95xfycu z1C%LWideH`fJq3~nKd55{%@|llX=ccSwM;Op&6+#Fg|z22UD$j%Z#adO&J}I&Ni!B z+2!!0*h#f4UV3xCRiket1o9GSQGmGJ?HM#Jigrbo*L&-pt|X`$s3lR+v-Y4C_FL{q zBTLt*f^s>}ioKaO3--*$hC1iVXmRtDAkuKBAx(O>KIElvg+>*HtFp*f{NJW>ecVJMEj~=BALj$HXZxm&L-Hlg12d z%!^aaE}+J)!cm1%e~!jf!yX?@S;qmqK2d0f?d_x$LoV?K z(ZBgh;!6>da>3L*KZ4&3qqqp{*(rl<<07eQJm8)E&SdghjrGBa+%)tk{(hn-+7ZC7 z<9Wn}zyx$yP-rSWcjXIW{k3mt?h7KWn`h_(fXNUX5*)%e3G0p?N|_5pc<{w>JDKuy z=Oa{$+&K`qU5`(4Zg;2UGg5j3bmchGm7Tk{%(o*QsCuSTt^32s_jeu@Pda^NKt;7W zib*(k&nVu~=#n~cB-iJUKUG?|(SAc|nmb9Dz4ke?qc3m$&h;_e{a>_me8J)uBOxRNIV!bm%l$~wa+i!a%_h^rYEHL~5!5qWCtsw}`PNM! zxv4uc@3WS#92QA;?JZ3iHeK-bi(NlI3~{eD{daSV)1cv`(fG=^3u1=olBKTD5KMN;-0$mq?(jS@q61VyP*^rnbDZ5OH!fx-&p+`BDOJ z#uvOp-TOku?rL!?{Q}2;De>-i66|dfCPQq{7N4QpZb;beZ@9b8B0!GYjX}1Q0>WW}#+>=0l>ndC%3G-7f&Vd9?_1)7Pt_Rkhzaqu5eh zqbJ_R^~_IQ0C+!NT@NkPx8KMD?ZtdQMW;w5WM~T}jWC;Ze%^*TbzV{RgNHYyG%THA%!N!y#T;@%O3cg8yaA2oYmRl0IRM;8H;mZng{aPhR-y`C2p)(GHcd>;d=;8A;oqV9?QM|>a-gt5$;jN(r6*8q*=6FC^gEZA}0%DxNc|EOBuToZ@0Af zPT0rn!cP3HT@-g&<+*LS8^i9pylX%7VkNmFoqoHXd2yy^F{62y2(-n>wQbXUUTl4( zH?VR3yBLt&ohx>sV1BJ+T5&6xLYzWV#qC>?)d$`+ID_Nq7Dn}C%;E52y9YRECUvpZ z$Ih^k!r>a6K{34zD`+R*X_xWN7i6j1ZXNG=6LsK zuM44J2gtMLaep64yQ{}?;>?MEaeU@QnAi21&sI~tVqISZtQTa};D_RVLRDW~Cr+$a zQom4V)GwtZODkHydXZp!wr?a;^?G^N4m}A@=BPNYJ z-5D`Cn|1^LXXD-~Ng~bl5VT^rs66ZMG5Uj(#NA%snROcx!{EtPNo4L;3B85saJsFv z+?NE5H%Y%H$4-2_9tmN{n#)Fz7emf1aB)W`sk^CVW`@Yi1pTyFK#krsH2`4 zcp)$zDg+6%JauY7jFAP(uE}1y`~`t5RL9-@>vD(>o@p{0Ye6cWsmneK>A~QF3d>6r zg21(L*p)0@Vm45Mmo#0Pr~9#;j&HZD=*G-6%kY*&``vA97jd1`C)C!ek{GqN2Pfw~ zz2Qy{!rPx3=d*;!oUDsM6`YnK$FWbF)(bum?>Rcud9jRce`Z!U!+OaWD4H#uOqkGT zr7$(;mAe9Eyhek#A6l4dTLTmX1IJ9LY}$1!TxK}NOFvl8TErp<4&$R98`&j>3ul`Y z_*qDyYmdk>ObV!oHBIh{bTdqs#Lt=)G&7xmq^xd+<3i6-lkGg*P33zNa?1B6!Sqfk z!`@ZVQPPU`YB+LO=h2=BW-_XwM0RCQ=u%ABU)wVGHL7 z5FTfC;BFu1rs{U8A6qHT#(N;tzBp&I+H5!7{^etilv zNbMJ!9w~{KZd&n(-cB6t7= zthpj**0!^Ix&mj~t&eUu8?t!EoR`p9feXdK&m}`XY8S^X;SfV?#tDp~?so?-I_H3{b|%n^%*tnfO*w4I(|#C$cXJ)FLkW)U zecm@$(ln+BT*5IIU`^0S?Pt;G&75PKby5z%-f_n^^VMp*A%>aKJSnb&Y3l5~!m*d= z#Cf@stZm9L8;e~zKvM9j^5<^0VrBQ{(;s+mq~PH!dq>o;p{>YPOxwfhIue>S>^!-DpVZ;TZc5yfXAeWcor!&E!>lO*=V0n}T@>T!FP zNO3R8oz#oL!p_q>pW`TOnQnJqn#LD#$YIs1K91SmS8{sW3YpoOh}ob}urE1{8k~{A z0fg_;m7F+P zvcGjcw&Ptp(bAyWg*cQ(OP4A}D75@EhsCa>i_l1y1D*<{Jl(p=V=!M#M}GjaVa|P4 zvWN)>gaWcW3R!D-{#mFK>~~Rq|B-NJb*osxtk8N0S@bw^x-BU(L0}Luo?Cb$r*oPl z6v;qdkbC%Xc4!yDLpQ`01TpQhH-30~OC($=?hPfeSER?s zaG~d<%w}ISOAlbaz0Tg?3WRrO+E+*Z=8-R1hI%_?S-2w;07iTErmxL(aq8mM5c(bc z((qP-D}GA@@|$9p7A7j}Qo&VAe|s;C%I3Z_6;qSizB%@lYCx_WZr6} z^v1~_`%5ayek|^qlf&J4;g)G+1S0RuN|JirMsv*Q=^N&<+9VSIc4d@2osu@@(I1K` z2vN~j#GdWrf0?s8$7ho*wS9kGRC$7g9rMqbAJ9TygIxqova78gTe8x7OYGi3zgbMf zRxuBe0B~OMU?YFuQP364HR*ylFQ;dsX2k{I3)659 z?iNBQ>;ok5yQyyPSR6Ks-5OYGz2y2QG1&?6$xoc)MuE znQbI8G2a=R3idzC4*xT0ROTw5eExy^Y_IMJq;B0EbhMO7(3V{G|2@pOWZGQ{QllTif}E66pAZ*8LF8;lR0;cJDFXpL@6o*VyM2R=`Y598uwBhx%`e!DzhFFPB zh5f9g(otHR4z>M5d9K?7b25gy8F;n|q7s&`_M-rJP_4ngS05&}+B3uk88CtJ$to{D~01 zZKYo9!-;L@4Xfq9g*t|o%O$sNi4lg$UXmuwWyGDxeqG4)&vs#ayY|JW%u^G%a zDPA%)hY-RA!u{eF{6m~Y=;@}~Y$_z%ger42bjvN*i^)n;t8^>X_^i|{`x%Zy z;Hty{t_4U1_-zuYmjSifGobc0EAugCDre%@?zooSQbqpun!v}2aF8rZbQSvX);2Ci z{xyGHitPiD`g3-FFUi1;-rui5RXi)-^||xSz%Mq>PK- z+J2qK-jeL6C!7p*xlE&r>awiQo(Q2^mlkzyWXwUfD9 zEcx>#3IUvLYq9uQNMe*@w5R8QPfd=x`~C_ym{r~pc+!lT`|GHNU7_Vf0}Tb$mA@_8 zC`(Lh+;?JKMB$Z>KFk6D{7K-y_0ec}+ zcZ{&!NuV8ILSTPn%RghGw{ry8Ib4&b<-SXkU!*~(^HneLp6!c9=yuO)Lptg=c-s7s z+2Nj$rkk8i^V2wa^QN)YSUrI#a%)@9aVDzC)H3pKtD!{$sILbMv1K{buxIxvk)R8b z!<&1-RHD4=;1shBot@zj!;RWX0ff|CS5v=Gb3y#G4*399K#7x!)~Tt%_ViBypxkyZ z55D-Xb)C4wcF^r5?i+T!#JwUXSG|y&yDm6CoEAYP&+bd4KLfOp@73dX7?oTfPT+RK zNzd55ZLWqvpk>|(@!Iy5;Uw}+F)zi^D1?R#ZN|((BH+yZa<`)&G%u;AD+3Fq;%*0C z7MK4xPg@|W^X+I=XujK=GxYJ{DQ(Bv(TEH+_wF}!DzCX*{YVqu-KT~nYo-;|r`EMM z79g&y6k81AF#@JumVzZyey)`-==scvNIix9;1M1|8IkKuNfRxgvZ%h=g=L4phRRdatM z3A?Su*>-A=l9N>%%F@uIcX}W`jCAmWg}w~w4fsb*N4k(B?8#!qJ1)V-`w@c4W`}{Y zH2bIp6-OrLx+vx5 zkdQSF<%hf^BBQFvCM64P{NAHVOk#z<7s?88uQ#c>`NAt7-_Z7IhZ}_+4btaj!Y=Hq z9T}hi=@pFTUtYHDTFCNn*j>2Lig$0u(RdcwL{BS;lGKMR?p?MO4+@fHo1T*DnYda{ z?`=#5_P(G0R;p0#l6AHZ-aRCo?uzQhJ1@-=YPC()bdQ4CfKiWO=j?bVpOknoJ%C?PgenlWAY< zO3j^UAr3=dTlkrGdxzAoVZ7Mi@2hcMJC=P2nPGe>wyop9yFL1<21^Dm1zAU}2&C?tTMbt8D>26w)zcS|~->Dmg>W{#3#81T7BvDfHiU zo-hx%02-f!SEi!*85}{d?&;h8F~lW_0-{2=#-RhES@!K`9V z0?P#Z^vKcO8HO*8H5)Of-r3ggveg&rx8<1wz#>LUu%)x4Wedd7LE-pm?*epb$3 zBIBz;sdCnlYGPLLNUC818DiiVU|`LIN~KDzJR@ePz+2E#*bC8fcem$3u;n0Puq@QD zo1Dm}GGPdZug1#f3jKD|ibWg1lM58`_67ps&3n8VM=-;+L zrT&P{)T|er6vDe>)tEJ&sJYF3*hfGwhG~K5BPBOcI@6oGrzoNPBegIwzra{c#2K~ZkE_> z56Cj`4Od;0pgUi!O1G*(1LGaSRECZOVl+CVKBhw3T~7WZq=pFGu9ccwS19+9n?PmY zobWuIGSt<3-r`!xlg@U$Q^?NUxiK&ADVh}+2{>q~5bbSs+{m7qo;oe~U1-nXBslrV zx+{83_bCo`)gxc%cKbPQqK*-96o9<|A@p!kgltSbDPqxv!R)T4wpLxL%Uj=;G_HS$Wo!*J0AOuSBif?MpMY zo*7|6A|(L6RwG|`jB#;p-ui0t>+M3GP*y*8>~2lD+Re@giekO{Lp-{_h3P8KQ2P-w zY|Sw@@>6nIzSb!2A&{}_Ak9N$EA*pV?yvyV`$){rXED1EsOic>;=Q?)i(}#**x)2} zUCW{{Er+K*up1^!HCsZh`in#fE=j;1Z<@P1%!wWSa$h&QpUO-3SN4A{eu7_$ZF0YI z9FnL5m#pktnmb8p%HWLi<@x6|fj+dlE|<}3yQ+QT7VYzQb1AI5TLs3OhjrLIj9iFh z!e7QLRCD-pa}Hqsp!0XCbYqw@<7mc0;&I8=C$o~w+hV9ekdeug$wCb^v1{r#;I0t@ zC}JObr7Q0$VJ^T1mbH!&cgfCodxTX!0N6dos>?l)@#|&IM0dPp&SA5X$UDM-w>_uH zFgK~wt)nI&mkaWyW>Up5ekv__w@vsP0U!Arn9{gju+oho$CbLscapy>i>-d$c+NI3Uca;Ha zDDmD2r=n0wwGC*c8Lt!B9?x&jplA&5o&@vxQk-@1vy=y0->l@Zl;vWIv{@CK6KZKV zRno6>4Ns*~RBd2KNmY0(wR%nV21C=TL@H^#PDS)|<1JO4M7z5nVcrq?o$eAuze!Z&P1zYFD0%C1lz1+v+X{PXH3-grH9-5aDV4G!`7d{0-~^J zc3(0HzH5dI1aRxotS|Rn5%H~YRg>XY3|5?@DOIMfIiXxWcuE?$ku7DV(wgC7m${45 zL9wqJxz$2-W0h%ET&!gr(Y%i?vE%zfJefN^=+x`_(#IGD?s)s8tM0d|V5!Eu zmqZ&Ne?C?gI4NXIz$rz2FYM7zr-qqF%sc~9w@+mE{MDm3_c(pHXpo*XcSY92Tdj1= z>~I)^ZLz%;{)!@+G%N?=9|jf+6>YVqm$j;`N30iKm@N}ounWn3s5Lel{N-7=DvW&J zo540bs41VfkDh37X_4LI_5&W+Himn>tETp~=s_gRVc@jtc2&T_$?1$AhM=H5;iV}S z1F7oHo9jjCvd=3{W85``vbr!Pn^uLp8*(pAV^l>O7jLAm_TE|DS3tw1<XYv@NG|t(+hmRUq8SrN za<`(!O@=E8zhJb!l(iMS45Tnvj!Y%lca!Vhc+gG5c!w+T0R+Wl-&>@T8v3(WyU2lC`s( z>EnW0DZRg2(pe=CJa#hEt8+!d-w3O5`#N@Qo?=7T`IAR=e_IIykN9&1XSCq}w1a?3 zzOR$P9+;lmNmvd^RhxzxdOvNgZz0>zNf;`j;JTjVp)E!x=i1lKr1`EIKKRXt6gn*y zYfxMP0jEvRdsA8Om%8oHX1AoVl88hB8DMOp2ZG)B2BEw+PX{JAVU(G~-NW&t0lbMF zpaH?&yi0+<_nAHo&UHUOIgPuHOphogsYHG;CVY}i7=(p(?C;+L@J-$|;rViGWnbTx z()N8M-rHuFEYFxJ4En?!%LIIz^h?4{rP`Z7L^x-z78{}6z zHB6;1!)!4z4E!hZtE;dW!GI_81PVOcb1a4fC-)LWqg+wYcJ_$M!UcUAlK zrtZsD-`yHz)sOEq2+|3&j_1yHtM2T~ALaguqV+<3_k-!U9?sB=G)g^9F_Rrdrp#m#c-BE_LZVHyGp54>a!Om2y9%V-y2 z@~-mg1Bh$@c+<$CcLKf6z~i=)$3`CSPSN`JsRN`&3lO6D_cS$>a`_Hg{muchUCC&Y z6#C)AiUQm-r^;+eQ$HoLsV{UQfVZ|;)_?k!P+yrzG;zkT4WMrB^OfY;cpWXRf#V5dDa$m*+YA(=+@uA^zKCpVH1*{triqVK6QrWjDF z1)$3o-Hql9({8!rydrqiwwG(fba!ml+Xi3YF!;CvIZ7+U|>*-xpoUdKegtgjly$^LyC0TMoqs6XE25P4t559d1jx?gU zBe<)(&zh{;i(e)`udDzZ0}BdioZancO5mO0=1X9-DQful+5p3ar%XUa-1`js^yc1D zV7Q2Qj9Sr!%mBplz2@ae~wadzjc>*ReEv(lWq15 zAh#EMkyjq1{A|+QRJPH)!M$_m#H#2XIo-WkKN)IlM9pjZf{1W#Px)d6G?ZOx;h{$J z-CM8<`gqFY4WqFU%*9LhN+cImf@im{lHWU5je}VW6>xb+4VcT(7t{UDXMrPKhtC!o zYD98p?EA4<@8o|t5f9WHk?=KM8u312iA}+nB6|h~g*0mQ@gBbEpm}9j&0fld%*y&nRD0jWN?G7ZQAdbx~A;RAz z7Qi&R|CHqFTqdi*AoFD1;=D-iK~Lp)hhAi$E76|FJ?P23q(#k!K=+PL+TD5LxTDI2 zN2g3!gm0Ub#QICrNN+GIX$>asZ=Am~B$#fZVua6O@;{cE5{ta#Cs#C6Rj}N#wPI;%NHihc0z;Sfkg*9uwCSI z1iux@@z6}$0=$#6yQvE=;-m5nIG1E4_FXBv64ItKBt$T5F4L{V%Q92Qpp`%H+IcK= zwPmO`@58K?7q;i_1bHRh5VX(B2HI7#MNe0dx1~#8xq0P4rR=n}e^K7sexEcU(g}dO z+B4)F8$u|gB`%w;le~+GXkhbKc+ltJeyPSa-iqAxb+dEdz0kKgq(SoSY0N8f3yJcs zdU3+mxpKCZd?$6cm-QZ-L7xn9Bg(EAZ*CVgSW!7{D(+`;+L$8i{|xCVhC#08>WotJ z8t!yb_V>ze?cTfaa2oH^l7&zupdWTB+J&LQGt}euZ&I(fN9u%m-4}J}jcXzNF)gQ{ zi^{4?R8S0wzlI<#cQ8%(T)(-6iw!@PvBWTIs80&{bX&(*vxZTgBC&P@% zZ(vKn1;QDdbec1`kn1y%S(98`uwSp-P~`Q46J#r3_UC%xP8VL6b8xy`Q0iSATfGy+ zb)g^+G2_z5210gsXV^!lu$$X0|6;;hvLu1t(rJ8=o+l>Fvw_K(l?-Q$9e)z|cHsE6 z>8`1f(=*=x5}nK*S0w7vYuxZOo<&Gu{o9lpP8Ws-hU2_A{C2=4r1F74Nx&zV`u*Tm6{=!-yH`@YsdNhc2 zgnRyh+c^v6uKStPij-Ix4y*6Ik2>8?@9jUF4+KfWkkW{i(sZJ_y~kR}^a~$d7BuKI zG4^l8g!AN3-wpn?u z#3ny?tr-(ptJ)hCSJ@3VORZewhFk8to)fXIUJW&jt&%UdL5ADehB}hfOJ!FyMz*-$ zN@n*anVddaivUMYCa{3U!ly$k|NnIK&aqZEza(J{K zeY4vyQC>7<=Lej&HU)%_{pv1 zNrw%EopN=$^3G?Z9TUP)R5+9OPPA8|X<7yvy}yK2-7NvuJ4@VbBKQ|6D&lrEMSDeV zzz7|!eNQ$2?CV0GcU3bx_EXn5wODhvCyhi3p|3Y9DWHBsLIrXIR)%&7Vcyho+{0Be zO>?ol^-gmJFY|m#nU$lOs{Gj)HAe8owj%;bI_%!20}TN7!VLX#)PFl;*Pm(b zT7Pq_p`Z}eUGqP59XIp|P*tV5y8dO5!Yi%DE=NjVgo(9T$T+_B?nuw*11K`=IX^UQ z?V?}JUmxve=@KKU6fKq%W5Y-^{dSjRPrc9FHwU0^8n5$y9Z9Kn5n{0EIZa*PY;8E` z!rNl_b+;fqHv!U`l}v(d<-(Or>Z;=sNCzQbsr6!1yZc_)TZ{X(i>V3yQXUlcOyJ1; zt<33c(w~i?evHY=utp{plVgq!w_zIbxl76sD_BmNI`c4X^)!h4Zt=|9Zd!&q>Dl9H zWCoP)FaKD>$q9TGG8_levD1V?p>HF zklmU|Yoi{LSd<*_xgW-GW&q?BpS;zc8wH&OAAcYH8CtMWi2OKU?r<5*SaYnI{1l$I zN^m9FE^;Wwd#Re>0@E1fYd=BZCgXs+dXgcKcvvh%GHAoQN;vug5(JAJ8`UuA&M@45 z_%sblOQ|L9qbJhaCC)Oyqr^tjtzpI>t9P4Jzri_ms`0+1_Y4BP7(>2oqD9@Q@^X(* z0|n}Zc3L^?&cmHbg=&FyGYXT+0^M2D4YGODKI;!e6ztCi9|sGKjKU^qri%n`v;UFp zTiov~lsT^$aNI_ZNkbaU!pg2SMWkcpXUTM5d};rLwHD$8Ds={SNoVL&tOP^gu4*FO zA2*KK<>4&1)Q-|h+(#k9&X%IbJKfmKHFq|Uzq@m}&j~BSg7%;1;)h3N{bJ{c>q+5Q z*x!79ze#7=QVZRdu<}DP&aLSgyH+8d;LF-FJ!i?y2>= zV6D-(DO*zNAZ6FfE}vUN)u=Z6;ONF%=jCQLQBH*0QYxu+Tg3V@lxd`~I+$Qo+BJvi z*1Pf$HU70A+91vwCw(P&rwBhDd&PDVVUB3{#PILOS=2P%<{e$!=0Z^ZiCd$bhA4(* zzs<}G&sJ(P8e5|lt8@o+K|sPRMmWgStrqTkx~p#cgSw#*dv-ByO_tmwwCO7gvhNEa z-tI_>M;^;JnADYzB<)IyS?n2V4R~2t0AP^5izKW!Af+&v79Lh+(fZNe-a+hrHmm1G zGZrX=j=VFcOD=68x{Sz{ze@t^&2gHJ(o8U{TyCMZNTC*=kAti$sm1q2XmhD z1RM_ei2WI~;;pesc!o~_W4l~orh#jcF4zX}VbBHa3ra8E(qo6k`8~?A^C21P!Vu#W zf?^O36&6kzgEpBX#b5p=4AAx4T~X(dv8^sOAx(*le;)=>UMFmRSng#+vVulc#I7Wt z*NLg0=D{-XG(E$10PFn}q2(ed$aL$=){kfq>~)Xi2<&UvQt>R!Z=E!v>^A^b+0jmw zFo^fA7${DljA1P0taAoI-^FNv0QYcf zXfR0dow0d;hH*@#J&=7MH09lNhoC3aW+^9kg3os2mBPw{Z`>H}FC_9?q8ULZZ~z~> zX(ohYQZL)qrGeR}DqNmikwTvD1qRwDBQ^EYHLsJ3Sw~NN$_jC_yV{=iQebyl3cf|?dJNtG(63{H+aL~hHM#TR8H?n{7Or=2x*j3XuvKyX^+4(0<-aKe#Ex871h&qlbD;)&Qt zIe`V|>ZmN^Ok>1LkgdjLCXG0W9GNDhUo;;|b{#{u;tqgrZzMBRw-P(go|U|r(x`4? z56<)75|y9CJaH6O6bTIXC@6o7qqg_ip-89O=_Ziit-B6wwaRzo@9n=PjU62Ne1K+6 zlZB_1luSB1xJqfBGk-Lj4l|pi0M|>h7pAZ@U9d>&WHMW}4hRR}tvP^2+$A}r93e8X zIaj2OE=y*a8qf9`FU)w91P766*RVd0!&vs%iCa0)II~g}`*HpNVC((&IYY{sx#dX6 zOiAd*8;R=fmU4zLBR(br<)O~9$i~cnw%{*>(+w7&dUOEMB=1)wHk1O1jUA^&HYQQw z+2=*WS46!*hg-?rCbiq!rfd%vUp9dX7r6xG!Io+t98S+Nzcvpc!~uVAs*$_9KO5#d zIS>_fu{eY~D}l7f-+w~4)v64qj-u+Ty&+jemh z2*$PEhPH)(RQK1a^r&g?(S213*Y3+HY8n%UJu;hcA+N{XKxO^5xH(W?$ULmA#w;qY z4$Yy+8$|Msd%K7N@63@v?ld(^c)8z6r_7Wtv-(9JzD*wsbp> zo`iyX2+|0A4IpRD*5Gzlf;34k=>%zLq+WF^2R*vtAdGF5#tCn;{J}{~c=d=^M=Iwo z&uVv%UgI9~rA<#thVPbhHc9fW*K86Ap7WLn^!eEF&6e&~Qfh4}c#krGzCAQWbib9tlN=%?5L*`*|68uEyWIt85>rzw@UCB>wyOa++C%ULyG0X89^v$K1+6&q&{+%3vHXuyvqU?liXh) z7qCw5FH&XgN6~eV9mhA+ujO}jZ@Kz8^mkS44s&N#z2olAu1xVb&6Ebo$XIi|!#AC6 z#N6HXB_#M~K?a1={T@aH4Mpp6H7iNKX@(*xN>u?rI?Lh-K)9?Lx)%zt@di5Kb;NEo%XMSM zNwRN>&A}2fLIB5#Yf_oZ)>|yeg2V%U)8bBuPkVZiqKHOpO^f`@K{ufqetf|2@W8t} zI_IB<*;}e2I5toj+D~tCcbOq%NT?ZJ7k&#R*>|PrTi7({#Za|m*?5z2Y!Vy83k~IA zT5OTON#xgg(;N}mN!>hbj@x7=+mst0o$Z^X=6)+Bkv*z>q0~?pD%U^E@r%|DNvfF| zZ1I~Y4SQ3L6>OAuwMTH$oa?`B!9=akh84EXMV3{svAas4yNc5xO`1H&hg}_V%$YEM2|0{)}XzYN6?&*j<@H@VB)arzNol zbI)=mHQO99jp=J`m8M%r?zk=9)@fx3cff2h^+oKrtSIOs!=4B4xs!}9@_2YTzu_C?EwjeESc6#MmrEl+cQ6%k+Q zQS2F~zHp5*t60iNT#zor-9c(&`4B7dfEv*Q*A#QGt?d0!thcS0dy_O1*O_xJy7=}P zmz5j#;VdL)cS(*XQ@sd2HaTyTKDt#ft<0!_Hwy2TpHheB2E{9@Imis79tt_UQv8o> zceyLcOb~=6+l(xde`^id&;m*eOz2&v=AxHxU$@cpcG}sAg%$}~^zxN)=?n^qgxTgV zEJb=}IIfYOir(7`EKyVWaEe+G*Az+S_eYd5okqM$B0xFH(R@8W$2(TRK{> z)AywjZCx(fM;TwmhkbYC&szS306UIt6er#Ng>0K^Sin3?&C9O_v^ony7T7_GU{R*E1{;gI=+UPWc%moBkR!nmK(Rp)E?uyJK7@;~o zqYnb{LM#b#5e1%k#a3`hc^7V`xCjEL`fk{vdNjT@_7Rc|oJt%z#b~YZuzy{O0mn&) zbnVaQ&7qI)JnR!E3>6>=bQM#rKljRal3w8uM3&n^>H1Lnjs7k5n;mKwa(`Lx;*$M0 zac*fpKDs{S`W93&zJ68CA7l~oZFOFn*Pq|+#?~?;YAtZF00O5R6Ohw) zEcJO}b|VA^EDmKEr;o;FAB^PgD@ROuMe~p^iKsXC2aYp`emtSPrunU|{v`sO;~l%f zuxzJ~*v*ALzRo=A%z9=DJxoOZR?_$Z{s!lU4UQ$650_-Ln8;Rd^q5A>swTHit*S0_ zqFrupo~*0UI~_vSjD1M8Z*i97t83s%x&@!J3v|50rB=Qvv@+QQNer|c*tLNCdKZO9 zPY(DAWCbVsSoC}VKPvD=R=dv~B>i`g(Z*L&G$90I6$sMq`pr6$s#XkG2zFnZDA;E> z{;cuB0PR8Uh-0}gy7>0llZ*cFqsduS^#ebn*%a=#L9Op$c)`cOx%+$_Moi2vpqvLh+WWd??&4K7lE zXNiQaoPP^7F?Rn22rSN{_lO?a$s6f8gj+;`9ekKyU=?P{Sb%Ji0zBLl?zxo~G+Nyi z7gOU1ZlR=^!K-<>kDs;=t<77IV^Zu$+)66>`br}3AW2ic4l8CQIoSsQlxvRsVdsk@sP%q|-+su-mytvnMX}(g9 z;iJrov`_a(vXFr&`w1B$k_p96Ep82oGS0Z+nIT=6@CR$`@5+*Z_GUOHogA}?SCssr zY8T~`%=kp5x2Z0e94FfkpbSa?$=VA@|Lenc%;%rJlQq0-RyCc0b-{b@}rtvNPf7; zO`Md~V%zjS0A<7Xg}E{4>Y81XDY30B>ZuzeRf=_`D2b4-W1Ky$@tQ_MShXd>{UGy6 zVKdJ7TM){peSD<%E<4ORAe=?)u1)*K^wsS0qmcJU!6c|y^ z9LJ|DoF%Y(=IVYQ%a=H8iUY;OE`HjT4C0#6Awli|)I`=SxundZx7|z;g|1Zq>Hk&| z?kz1g6+y2Hh^iE~l8*B&);-WL08EMQR&tXWFZ}$CsATfER<_TPyxt`@pV3U)lwy?& zfbe0N^t5S3T*H0{W7-#*Aa4!W&u{+jZzl6`ES7cN9TVKKjN-kNqB zBAvJ86#IS`4L7xI81e~r)6hcG3kZ%I&pZ3HXz$QcEF6PWjZ=^0R#Iy+-|6OHZV~Yo z`vA!5-E-admKae%YVTkVJ+gXbUdx?~u3=SsXVk|xg%HC&6LYU>5< z(+9tbUC4yCl;fxlc)B1ZyNW#%XzlAfxaavK>Qlj4Qk7qS`ewPAzw^9g-x#90JrtS? zeQ(ikWc&C?@4e)s($$`Bq<$r3CN2=^C|7yVZcDAdSFLKvA{R@M%+?kYDsxHNK}(4Q z+*OsoHrw&DjN_vmZ|@nrFAS302TX$Rn6b|;V%C6SyRV{=7L(*6rD{5ow{z!`)bk+* z7vZpkR&y--X*41*d0p%RYgWO6`#PGkswA>h+fNc@g?$(#`+7^Yr{27F6347bb4euo zwiQN2f#^w3C;!gT_#uyena`-2ptv@I4!=9;<;^H056z$yw??(J^O7W46RqEDrgBN7 z`ZimM$XW72)U>AE5=r%4C5j^26^!C&HWcf#kcQAv7KWO;%5pd5Q^~v@XJiTDSaQ{1 zXD^+?prrpN>`Rkv*HLCCR1qam{|)8Me;{u`;(fp}s=qgmuGhBJpd_3Ago=c7ezeoB z@euptUR%ShNCDoemK&K>=LQM2gvAGH@9a0j6+$6DcX<8tv18p%P}le@;W`QZo`#GhYR?!kmH4!=~I07Mlwz;1hl&@Rvq;S3ueqByI%Z zb^g;n9y^4pIt7CkvHHJ*4B&2vyg3rC3clU12>0_v!QHx6j?u2i`bOJLZX}qRv)z0i zCkVx1hxonTbp16o7F;jG@!uF!y=`1rA|3h`D&fdoP(9LD-U$MB8k~yVkcy7+$Pd1X zLqQFr{(XkkzEq1a0IurX-2I#cmK%@e_eV2EruUtxWuY_O*R6loVI4&QH-VGyn)Md* zV6?*(3Xrw#(N?4w4>E_sbcUaU;-23)GKe!HtZ*UHY`kz!wuT#yq+a}+^o#4 zV1_egTdHiiIDu+$9_2JD!q)`!ti=UMH8@%Uih`5FT)C~XGAz}vZ}|zT$S&Jq=vmAR5`H z8bHo?-;eptcQB`Fxm*L~HlT@IuF>@yiS#x4jL^6X9y_5KZHJTg}bf|om9uEmuA z@zz3@ynv4LtX8K4`HjSNDa`Zt9#9ZUU40KxHv~a5sa^7n8+aq0jQAS}vooo>-u=zz z0POysPteI$t^px4K@9LB!OgeseDP0(?3AcQ;oI*&G0rDAB3*HKZTi0pPQKyvd>a+X z*&;jx55*86%wp2TZmt{1-ogVxY*^Q*tz=qY7b)VFa zORweLXrdDJKcMPEqTSJ!Q>um=GawV}cVkvLK)1jC5kOw?@c1sX)f7|_j?(>FfTeFp zG<;V-0@Ny(euoc%`La^6r=nYMyA4wmmcAv>8s5rOyK?^ON%ABAZ8D!~KZaUBO%48i zK6p3O^_9FhrE;Zdf_FEj{`FK7s7W<#GRx-l8w6Y%sz6O&nOX&8+yF9>BD@?VzQkup zt}X@&TOeq>H8eZM*`bC*T_^tsf|fbkr6*99sT$a?2GDq0uX&=c*r6%X;3pdJ-pV^) zMa90lce|}r5m!9xjRWsG7EoK1}~q{Y;u3?|)_emt&pFiw0mVubHg`xL3Ku zXBmskQ)TLSjDf0|e57(>rPHegR#*uN^s0;?S$y!4Z7Q$#GX_o$AvE!|_@)LP>0ArD z84P`SWhcD67=W*yFYp_;a6;rEGF4?h%JA~bqFSQpyHBDqbFz$vzk?IVySy{M6w>~A zeF>%%ZBhAB$qCA8^shyhpO=7m!#;MDAKO?~-F5N^e!hF47WMT z#rewm+U(5$w0*951SN4hZfI3*-@hPuID{L>JIPrHdg!(IiWf!>n{#0{@&cz+^?srP z;tl<)`ECT7&aO89_aVJ)eP&T;9KF8IfG-hvyfu$0)4D(jc00^&z`K=l>i19vaM4K< z9yt!3CAW`Q2)#sXzxj4ly9wol%R>o>4MRmo`% zFQa|V9M&DY8^#GF+zOrGQSHG$mc9=# z?iIY|Z~$3Y8?{vZAbY3O5aGl#d^Zg(l)4N_X0%rLr5h*uz7WehUJFS~!oLLskT<}X zT+S$0Ig3_+c{Y6Qa#f{Mn-1R%Ao%sR-Y)ow#adCghAGG^SAg`+Q4}OnGGFo6aG<=K zcU~%4`6+h+HxAoNP=LNG>_?>4!``bXZyibA5PG8`UWz51QFmLv-*IGMuiIRB)lml33xP;VcIY`MKxICS#;B&9-axtUCVQ(0Qx}A>8^9E5HGcQb7>Zr zZz1`G&rjB+?w60qfZBax-)|u75jo9dmM0E`WBW9j%JeTNC#$u^5e^pu{~^ih-}-5` zEHWe**M*k3-F4euR_EidzlOFJEL{)x?%gUGZ zaESy^Noe?hivM!Y^L^>Tn`@vzvf;n|mM&UhxJSe3$SW)UTgdZs_#@M@Yfg`^rMay? zl;<1$sF>K{Xfuj06d%A7{FY!C-2kf;oREC>FFt0m=D})TP_(TP2sMOo%XQLB@YOx!`DhI3EpRrBx068PvEqCog z;IcM2C@iXBI;9knU+TN2(X`=#@Jzgz>k2lZAZEPq0TB+KT}Q+U$Q|sxo+`uCp@l|O zJuS_Y%@>L=K*A*pC>K8Q-_Qy}@)xlf!=1@XrTgE^`tm;y7fx z4+k$$;MID5t1Q;DZQ;5;!Fm{9c7U24FSDeEdpdl4Bgwa(E?sg@-{FK<<;=HwQ}lW3 zU&lVmuQHI}UB@jH31T%O4@5DjzMWFNZT++49bPiXh;b{s=dqE!nrqewzZD?O{uDbX zaazR=p*6w_j(0?t_`d?XpBi^hfWoWi)e}F9U9TK&p^;3qjJ$sKjs?4E1(&64&v2# zc5ao2P}TG|9Ge@pZ=Q)*#^dyVO@<^u793C#1%VY~%`I*I(lGsoD^jkD zTg&6|BtFgbZww;0q;h?4((utF-!G_^&~`n5M9l{OqUA?|*~|TsDO}LRrs+GGRgqs- zr{?!I&Gms&sB`$8{eC9MyjSpTH1YPs07PVrS{1=35D*+5IX4j5SV^-Wd3h6LUb;W5rf4(kO#uWadG z@51DYQ3P5EUe2XmoN|ohtf7nlRvIwg9(Is7F8Jhs*X{=bbIGmsRrrRk(2O4et;oI) z76&0P$@|jP3Y!LP9VhWcz}IMU7zTq?f2Vx&RU1MrtM6J?MRk#)c9dV*T2t_u{H9;Q z;hp7X$0EE@DMmdgTVc=;B2D>?kxENltQx{j0;>k*6ig`3mGCu_8TIU1H~rwB*Qo>9 zDx(`P$0gOc7=JCe3{)VxW0u_WzuEC7)twu-k-4*SyXp>4(BS{1=Ek|#A}_-bJ?2#_ z6tOiWbq<}*P{jKQ1Jj1ETlUkXSnpdNQ>_&yFpIt;^a#0vnzi8u0hwFH7RzSLC-bt2yCS`UlvgdMM|B=xVO)N2Woyv%OMyn5j#~B?7yfz;k*Ksdh7* zO4avjFroI4>`VP>i0*EAj)hwHsqdFRbP>_Ezinl-Sz$LIPj9aXXbX+FH(QU;I_B!B z)k57%FeDM!p?C>kaZn}#Sw>Zdcg&J^3&U;SG4JA@Qx+<8wd!_wpBn0S<#^-@6wteAp9%!G&nzTct|+>r z)qs;#g>Y+Yx!1~HTp7)bL7X_4wB5wq`Sd-0PLro2?g#Y;`yxx@@1{LBi9 z7-vUWQE4Bj@qXpcQ}B4w5<9Lu@vb>7@@8F<^%`!eN(pcyt<`ul(Whs__syR==by~- zfN17lNcs*t6s#L^ix$C6%5zQfa|XV=v+he=az~J7bcd|q_tt>q&v)MS#P(gCez7j( z_!UY?sTx_Y@QYu<3k<5!F6>@L>%e>C>vy@dm_+J|)OkNi7m$CSSH<5HTdIDYDW?Ysf8f&gW*Smdqn>uG^>Ikf+lxA=r ziddL91UD#JNn!+rItqzwoZh#J+PJIs#5Z#)7{0*@jQJGG2vpd8J=6lW`xdj&dcq3?X*Q+|?z-@&A8FYX`4_DEMxpj1KN81N$qSs(em&>DZv%2 z^hH<$;#=}9LnIm%q0wGe+<^G15AlaYL&YG9`7Xbv8Zh5E%cbi`xc^dc4QU|x@+vu^ z@LL*4w+K(He~EV=?(f{qHnfK^C~E4wbkYs4kDmdKy>0bNlBYRybD$MB0ug4H3?#LN zgyN!Z7_1!DzF49NH9-_MGYr(KMu;LO_^x>^@N{n7k1>JUzL@?`#F+#;jmBm(R4&j= z1=XH{F=_+1cjTESejc%6$3EFH#9D+v!8(6*aKRYYgEgO@nZNrX+IJTfdemAFEvrDB11sWrn zqU>)XsHJqPEYuBxXYbq>9c8h;5QUZgnD%}Lbt|T(O6l$zj;-QU8a=0jSy?f{HdYZd z;JYB>#P9jwWj2Jdl};CPu*btATwhwXBk3!Rt^DQ6>Sp2wnVV3_XIO#wClH?6J;j`QcVG0Vvoq3C2v=% z^6`N_!|T*64l(X+{m{SYib0s-E2aYbD+EZ3Tp|N?@R*KxIQ@uJzN|I{Dk_x z*z~fc0dahz@Lkz-LPf9!8g&Ls~Ll*ST*o=sqD*Z5h#4CvvwFs z2Uf(V1CAnM&C~T8j2t*;f8dZnct-g6Fmi}mt9cqxsV#75d$tLC=az|J5P7Vrz7*~p zaB_(7-CH8ks_MxpM7%V3^#%##(b@&`o=ccH0DOZd$J>dj!$Q7=n>aw<(!V3U<-k@W z_wcCwmuz=>QUX}?)RDH%h;w>V|HQLEtz|qS-9X*#Asp+?%A1e_hoqxsCJr?fp2_`Y zY8m69%(jf>y)RT+GU~x3$jb_ zn@iaiXbv(<-dsxi_v{a$i6}2!jd0h!#Lo1lc>!KlQs3{__1blbb8!tj? ziuD#`H_&$_#rV9n=+&<)4b)%3ySg`&=!STxxQl+pXg1OdvP|Z!oabT850ABXgs`o? z@{^1qS(`IF5!vQEuF6*ZsiEWDk-=7W8UjU{3abJEPm=BM=WPMxd$Zwi*HHnN8;EqR zFV_Gq#sU`k?!P z4G2T-zK3W>Wj1*6RtJ-}`xE8eBWc@6xhwo-=urFC@4-qJp|;ARMtI}|D?PsgG*zc= zi&b`irtU3zwtTyi&@xqH2@l+0g|1wB{dCoW30!BGXyy@}axX@34VAF+%&?(kXeqk5 zRfTb3b7B6oh5G)jLuoPn|RcNHyqU{#P9VG$M*Iw5}OawYS52Bgh^?6EC>V5U*2e`v(+$ZxDrz4 z(8BRQ(ad>w$sL-W&_rJ-ce7~NH->wC%(#8k)M3%kEjC#LNA_(3<9KuUG(%9rumPwDg zKxz6tg1c8oCY$(If0~PmKdi3Z9sWYF8d|pKwXXu|Rp2yrOJo~a_kL;9Nkf5J7zY*= zWw#{Rf(N!I^5ldLus3GMwNnkNBYh3qVb##mNdwZW`pSB@yAEpaNSW(`gjUcyTc|{u zMc;7R_TSRsRMWwbL)t;@4Nk@f#BT{{JUvv=f%FDXf_<{uWMGfK6*ua!F7{=@z|pia z3a@p8zMg6$|DmEBbPK)1=VSCd#oHG-%sNaPqE*w|2fGeT zJNBK#-v1d=eZha~(}hgv@Gy9n2|eN9 zLa6VgNI<6V4|Ici$Nu!kGoKWuFePEz%R9Xh$hX>Xy7O3lH=MW3#P(CV7;u3JKM@4Y zowBTvPp^a|?qMYENhNhwV|w)ZuWN`xy}UMEJgypPE3tXBqQj&irozYe$hN&v2ACi#^*5pCX5j1i9Y>L9a97>SP38kb4dkda2)w*vd@f0DRUtgqQlNDK(e zV1mxzck08DiB0w8_b|nGOE^{D&7B4&Fa<2S!=xeP&Aq3nmNIV{R)1bS!^>{RTtLH=DGX_iJEX^^Gz{cDIjBO1$;KbO;|2#9rc(yCW%=My8D)hBiSj zmz|WL*{$KO1)qjC>%|*h%(2vuD-`(Kj9BN_)}}J|s?43){Z|0fPT0Vi601UcNxuKw zEtsulei`6`BBsfgXQLozYxG<3x(&(Oejv`KGA_(s!Vw&8=m${Wi{~%>*=SS9=o-Z? z+NpVGQ$KmJ_3?$>czC@#M?HiRpj|pIh8-~P9pfr$gu^rkG3+SgBmupfw9qv2^84E; zX8D{45bCk0wlHmmk??$Oxu#VSD1j~)h1X+Fn&|}wL^E{EJpw#{0E@@KCydw>w^Xkc z@7+8U1?C&sMVtz|USGoFPGiLO7FJa=C-o=)pn5cK=peA0eL<%%5*Bwnz6+E(AEUbP zJ3FwzatS}L==0Vs>g|@bj++yQ zF5}>ThR=zHo}VLe`l5Eg=V9k&Qlufu>LTW0kHLUwN`6^eDgsllnfO=gEnAiFhd&J~ zPe$7}ei6dqp{*R6)wpz3m#-c^#`>dXoEir;&F|r>g9+X~x-fG0H(ApNGo_+mKp)*6 zT7S~+63!Jszrp&cpWIMX>ArzEDj(^j?<24SY>I*Gqnb27pOtEPpv6J(cORhjia0Ub z0SU)-TzMe&hWJi;`^*UiTwVLZNd{`~DkIO@mbR97AW|4-yUR#XLv8usPzf(}!SOiv zT58HzQP)Mkdf?K#QrAV5>!L99m(bC`dI%=;?Zmc}_YjPlhIu;h$P)x5&=EqKFc770 zqnnb7EKJG!Nex7`QEjUPn&9OBLZH99(oGfsN+SiW=NJ$ataZt8Wh8-epn4C0z~dsntjeL* zSk-Ww80a(YebGV^SS8$iLwhI|{JY?lOH&M5(azzCa9`ziG4$`Kffb^5BO#&<(F{k4 z4?-qaZ|XFF2KSz66Is)z)T%5(`e{M}2h0w*fa8g?9D z!is-<34Cw{2V22`^fvOl+tHh3C6>_kjM@Rx@2KUXLV{rMx83;90QPSPc71If;*+U; z`x*>ihp~Uhhld5h^BgwKVGN>*_ht8HyJg24rGddRTaJzvI>h}Ii#!Yl<|I_IsE3ej zz$;wM00L{U;J4MKm`G}zn%@n7vnm5OVP~odhM_csqeF-VJTP40XsCDF4Bu?!W{`hW zypU}$0Q;(y%ZW}^5Z!&{vb`SvsLFqHS82~uL;PEr+a|% zJ3{V7+36=D0@~E?B`a9Czu6`OcD%u&>xjQ7fTL6t=w2xl`1OkgsjfnLz4Zw1RD_pD z*%!E#jny64vJ<|%BNrV=N7qCL;k8VKcr~x*`?UycqRe_le9;ga?iSF734phAS|71G4L~sK*X}Gh zczU}STZl)2Lc=?}*^Poc&C5bM6YNmJ?16JbNHF8mu}LpEfsj1Syn*Yj>D>(L^IXvh zrZ3NVK@JEfS2$lM=)Qr0%f~mtOpgiQ6DV)z{35SQS@#^@S(xx}xRq8ShcvA(g%@j$ zrTLPPXjWQB@Ok)&7JQRl{N>{lC=Vn2#E&S*at0V3ZGj1xH&P|u)2*5)wR`8RX>8w; zhkB`}G}8IJkYqBnZ94(8+z^tU{WP3z%?l2xdlfFq6@5=db}h zgx_~OHSEM7cJLqkx2;_Qmf91r@ai`Rp7WAdFKS!$81~4^gm(jPz9`70uqet#^X~=ry~5A==*sZp zzD(O+_b04-xz>xpqSR&eol-i1^D0nqRMB1aLaM>>vY%NYHK7bvvc61x_tPgZUTT?Z zx&C?T^-La&p4EaVvS3T06B#docB$-x%Um|kWbGh$jC+ivLx(Fi_$pnsml z%<`oRBxfLE)4P9QLvTRK8p1YM4^OuVj&C?R#~G=h&U`(*)qz|P&dJDD=vA?Rbw3p( zyph?&XiOb+(;+rxD#nY>aY`tca#q~n!YT&P+bUf&FBLa^T4B3+fZmanGE0s#`0aZl z>g-6rS!pOHa!R3nC;xgt@C}g7d$I~B{dR+xiu6Xt^}7(R@Y&M9UpLTlDsqPgF^mD! zv%<*>f^P^t;Dd*WLqje3+{l4;FlFsCdc1qo8r8iW$d~+ut$(zZKt>L)2Mq$cIcJGB zTveRmq5#Nu)*RNX|3`3ms~)JBhWK8DEK2f5*hM%~75A;>JCCSnCOTpc!{Z<;*!@y6 zh#p1tYhfu517UK)lJ=zZR!SbAo^W`mIoQ`URY*w-o^wA(|KnpGPCn%k;wop$jWO*~ zUaP~s0ExRs6o$*faKD+@7LZcT-Gsiq#lNF3DSFoZ5@Eysud!^4VP??eO0f0ECYUa-GIzb1$M;_J2Phh_gzh`!^Hr&?xJzAs5K5%517v4 zKRL!nj*gN%e?Kz3K)|}u_2*rV7}-9mKIei>c%!WE*l^x@>c@NtzI@LQ!TBCuCO9yh zt{m;m$t+&v>pSp!+s-v(a+d6u>&HY2Tg}}maTAmkKs~$_a5Y=XVjNF0D35?rz_B-m zRva3f0{LEO08gO4TBe=UAU#Bg!jq7k)UMW)to3>Kjkt;Uwb9n))-%vgqj2j{(D%3P zwM%vWxY&;x9ti`po^@5IKkNkcJaz*u74+(<@+1KHLjjHsnsxmF!uD+)%J2cehRH(+ z(DgGFRQMD+TgL3Un~kVWGy}xDJS{ML1558!>X2GL3T*G|&#fZ36 zujPKLUIloauX4{TD*{~R4o{qD7iR1-(upmz%;QoEv21ep_-t`$%Z2n_HN($DC>!4p zqXt*#5RbhS==~LGseWC29H;UKmj@=70=-Jq{qKi(nP0)!%h27dPm3T|-5~Z3;}B~y z%Ox!naq-Jn(hG(T0Wjz%hzYWdr!6i(yF1jGuPu;`{X4uId>My#d_}rJLy8V>c-TC+ zL~oZSGG7uAr_jHFP&-WG0o~Q0wNF%IKagaIn^=H$1F!vcZ*Q8+xnJP? z02qlLc` z%V8J^*w zghQ{c^yJS|$|yQ<@w2TXu$$}YYKhq2@b>|vTWLS7XqJ((RI4Y~EUB9@$g*JG5Sp6@aS(?}jCNh{=`AxCgwjJB zqZu~dcA9_l+YL-Qv8oO8h{9@!PNJ5BiS#gI!;uScSGKnABSPJC3y5mr8^V9J=P?RJ zVNX5e!}JEYOXXYOE_jK$Q-m1<--i0h;3}GDu1j=++&)p?g zuRr%9s$mg=yE{^z0en~0C)v71Y!{oPLj2L-{n@ z<^E(5_}PU_wvx(^x=OM%nl#k?-5iC=Ws4}F(M8AHWhTAt+wxtJaYUi|EAWbuY~y_m zHoe*K3f=|E?xv9@B+1&*i@I-&Ekv^IB*a!WYMdq^&||@tmvdiNWw>og zW$pafC{FJKy#U*m$`@loDRNo%+VEOH?+#e_;gn>!Lf{3hVI+pD!=$;h1fI?UQ#X2s zzqNm46-4kKUSR5ON*ny5bK<9v_frOOY;x;7mjk4YnpqN_Hqk4R4&3*u(+S-@+Oib6 zRmx{(^oS3k%=Z#TRwO~EF}9?R4Nb2New(v!yE-faIwNYeRCh z=f#7jSRiyz9jpyT<}5dZ`o|N$hmjS1rSvXMQn~20DnubHlp@`-K>`)%g;=2?GTRQPK-;4I%T7`0RdG#36>LtldRAyCn&*yg(0ME6j%u(lQh2wzT4=+tp-?ti1wDXFe0q}P2D(drlE0&) z8eB%HdLGLLo42&oWwdD)rWj-T>UUOf=X(fyF!GLedittncB*@IUxNT9&&>g3i-8+BcdvWd z@av_{d4r|ETbNo21JRjou=Bd8@WVj*-OBt7Fz;UC+!abr z;r8NRY;*3VaWTE`&_&e1wA`@n-7RD2A!MPzE#GhAFz;2|_v;R=KZD$%H5^bKcc>tTS#N2wrC-H$0o#VyF+EfVxNcJq z1CdBPQvR_1R^=Ro*t!kB-dS!UQbwXF*AYgx(;W2!Y17o15RtJ#=M7fhe9%A#ghs(v zb;G=2P#);0CQU%^!~B6^Z)@$A2xw>93fqT)!7UHn)=rb^+Wb`SkVCe4Iclnz#|8tQ z4aeR_>t;J!Ej_!3k%sW~Yp9@HF`GyEHYmL_IF!_r_oYUOp~JBUPO0^61}gdk(JX|L z$S~o4!*`cR?cx4v%zXhr32-!ompdTim6MH#e06+6Mw>^F$Kn;SBR zGi{Ym-mvr(jeZbZEjPHlMXoQ$j{~V$IAtR|0{l$o)EXUFK=Z1Sz3Q=Bz5u!Tm%AGh z#rCpG_LrBbARGV?cvASgjO0KU$g!I+u!kcWPQ5`=$Zy{fxzVXoFKi14XJg7;$EHfs zR6-uMX8^%+%Syr~*<`t)FSSADm8J1T`qr3OwXpRs#!Pxgzd$m$t%Nj%4U1kOj=ke= zEh&QH&bmEw>MbIWxu~k`6^7KK>U>vK9k=%y*f)DysB9WyRG+TxlukV=E9hP0~UC1_J3_^HOr^H&L6a;vZ>l zhwSER6SgH9tD8sXWzpQ?-LTm7W){Ip*a^S^`G#s%Yr{MR%?#F>e z<FT06yVQ{O5ys0tM<6q|0z__wfMsj&`#uM_ZS?omDGF z;vFfRQPp8g1T5Xp7R7si?=&<2meUoMR3s8QH?aP>?tMZU{mDxTzY^D>p4hIc}sqSBF#PO*kGC;xlSLGuj5PEwSIOY zm~HHlXNbug(w>4J$7W=g6}JEKYsqIR{02fzJ##&z+Eiu&~j5cLie@hf- z+c5I)rlPSVfb?;7gUCnp(A}>KIP|u5mBS8r7>%&8RWos(xf2~dl!E48iUPbmuEYS5 z�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оPYxs8;=}XoOiB}(>f`Ld(?b&-eBQls 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 ZI^qU_#cx#$?Sj;6aX*L_^|*0 literal 0 HcmV?d00001 diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-29.csv.gz b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-29.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..efb8215dfb84b5eb29e09339d6b68ba0bfbcb8d4 GIT binary patch literal 337383 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�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 RJH_wE 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

C5$< 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 N{y!#RQVCF86#yQO%`pG~ literal 0 HcmV?d00001 diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/marketing_spend.csv b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/marketing_spend.csv new file mode 100644 index 0000000..a58cfa7 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/marketing_spend.csv @@ -0,0 +1,7 @@ +spend_date,channel,spend +2026-09-27,search,1200.00 +2026-09-27,social,640.50 +2026-09-28,search,1175.25 +2026-09-28,social,702.00 +2026-09-29,search,1310.00 +2026-09-29,social,655.75 diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/requirements.txt b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/requirements.txt new file mode 100644 index 0000000..8a1ca78 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +duckdb==1.5.6 +pandas==3.0.5 diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/grade.py b/evals/airflow/cases/authoring-clickstream-claim-check/grade.py new file mode 100644 index 0000000..8e410d3 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/grade.py @@ -0,0 +1,157 @@ +"""Grader for authoring-clickstream-claim-check (Airflow 3.3). + +Primary checks: the DAG imports; extract -> transform -> load are separate +tasks in a chain; `airflow dags test` for two days writes correct page stats; +no XCom (or Variable) value written by the DAG exceeds MAX_XCOM_BYTES (the day's events +must travel by reference, e.g. a file path, not through XCom); a rerun +reproduces the output. +""" + +from __future__ import annotations + +import csv +import gzip +import os +import shutil +import sqlite3 +import sys +from collections import defaultdict +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "clickstream_page_stats" +DAYS = ("2026-09-28", "2026-09-29") +FIXTURE_DATA = CASE_DIR / "fixture" / "data" +MAX_XCOM_BYTES = 48 * 1024 # the day's filtered events are ~1 MB as JSON; stats for 32 pages are ~2 KB +TOL = 0.051 + + +def expected(day: str) -> dict[str, tuple[int, int, float]]: + views: dict[str, int] = defaultdict(int) + users: dict[str, set] = defaultdict(set) + load: dict[str, int] = defaultdict(int) + with gzip.open(FIXTURE_DATA / "clickstream" / f"{day}.csv.gz", "rt", newline="") as fh: + for r in csv.DictReader(fh): + if r["is_bot"] == "1" or not r["user_id"]: + continue + views[r["page"]] += 1 + users[r["page"]].add(r["user_id"]) + load[r["page"]] += int(r["load_ms"]) + return {p: (views[p], len(users[p]), round(load[p] / views[p], 1)) for p in views} + + +def read_output(ws: Path, day: str) -> dict | str: + path = ws / "output" / "page_stats" / f"{day}.csv" + if not path.exists(): + return "missing" + try: + with path.open(newline="") as fh: + return {r["page"]: (int(float(r["views"])), int(float(r["unique_users"])), float(r["avg_load_ms"])) + for r in csv.DictReader(fh)} + except (KeyError, ValueError) as exc: + return f"unreadable: {exc!r}" + + +def matches(got, want) -> bool: + return isinstance(got, dict) and set(got) == set(want) and all( + got[p][:2] == want[p][:2] and abs(got[p][2] - want[p][2]) <= TOL for p in want) + + +def xcom_sizes(env: dict) -> list[tuple[str, str, int]]: + """(task_id, key, bytes) for every XCom row of the DAG, plus every Variable + (task_id ""), in the grading metadata DB. Both live in the metadata DB, + so stashing the events in a Variable is the same mistake as pushing them to XCom.""" + db = Path(env["AIRFLOW_HOME"]) / "airflow.db" + with sqlite3.connect(db) as con: + rows = con.execute( + "SELECT task_id, key, length(CAST(value AS BLOB)) FROM xcom WHERE dag_id = ?", (DAG_ID,) + ).fetchall() + rows += con.execute("SELECT '', key, length(CAST(val AS BLOB)) FROM variable").fetchall() + return [(t, k, n or 0) for t, k, n in rows] + + +def longest_chain(deps: list[list[str]], tasks: list[str]) -> int: + down: dict[str, list[str]] = defaultdict(list) + for u, d in deps: + down[u].append(d) + memo: dict[str, int] = {} + + def depth(t: str) -> int: + if t not in memo: + memo[t] = 1 + max((depth(d) for d in down[t]), default=0) + return memo[t] + + return max((depth(t) for t in tasks), default=0) + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + for sub in ("output", "staging", "data"): + shutil.rmtree(ws / sub, ignore_errors=True) + shutil.copytree(FIXTURE_DATA, ws / "data") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is None: + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + return + + chain = longest_chain(dag["deps"], dag["tasks"]) + grader.primary("extract, transform and load are separate chained tasks", chain >= 3, + f"longest dependency chain: {chain} tasks; deps={dag['deps']}") + + env = g.airflow_env(ws) + runs = [] + for day in DAYS: + runs.append(g.run_dags_test(py, ws, DAG_ID, day, env=env)) + if not runs[-1].ok: + break + all_ok = len(runs) == len(DAYS) and all(r.ok for r in runs) + grader.primary("airflow dags test succeeds", all_ok, + "" if all_ok else f"failed tasks: {runs[-1].failed_tasks()}\n{runs[-1].log[-2500:]}") + + outputs = {d: read_output(ws, d) for d in DAYS} + correct = all(matches(outputs[d], expected(d)) for d in DAYS) + grader.primary("page stats are correct for each day", all_ok and correct, + "; ".join(f"{d}: expected {len(expected(d))} pages, got " + f"{outputs[d] if isinstance(outputs[d], str) else len(outputs[d])} " + f"(first mismatch: {_first_mismatch(outputs[d], expected(d))})" for d in DAYS)) + + sizes = xcom_sizes(env) if runs else [] + biggest = max(sizes, key=lambda s: s[2], default=("-", "-", 0)) + grader.primary(f"no XCom/Variable value larger than {MAX_XCOM_BYTES // 1024} KB (data passed by reference)", + all_ok and biggest[2] <= MAX_XCOM_BYTES, + f"{len(sizes)} XCom/Variable rows; largest: task={biggest[0]} key={biggest[1]} bytes={biggest[2]}") + + again = g.run_dags_test(py, ws, DAG_ID, DAYS[0], env=env) if all_ok else None + re_out = read_output(ws, DAYS[0]) + grader.primary("rerun reproduces the output", bool(again and again.ok) and matches(re_out, expected(DAYS[0])), + "skipped: first runs failed" if again is None else f"rerun ok={again.ok}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +def _first_mismatch(got, want) -> str: + if not isinstance(got, dict): + return str(got) + for p in sorted(set(got) | set(want)): + if p not in got or p not in want or not matches({p: got[p]}, {p: want[p]}): + return f"{p}: expected {want.get(p)} got {got.get(p)}" + return "none" + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_json/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_json/dags/clickstream_page_stats.py new file mode 100644 index 0000000..565049f --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_json/dags/clickstream_page_stats.py @@ -0,0 +1,79 @@ +"""Daily page stats from the clickstream export. + +Tasks hand each other file paths (claim checks), never the event data itself: +XCom rows live in the metadata database and must stay small. +""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EXPORTS = PROJECT_ROOT / "data" / "clickstream" +STAGING = PROJECT_ROOT / "staging" / "clickstream_page_stats" +OUTPUT = PROJECT_ROOT / "output" / "page_stats" + + +@dag( + schedule="0 5 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["web-analytics"], +) +def clickstream_page_stats(): + @task + def extract(ds=None) -> str: + import duckdb + + frame = duckdb.sql( + f""" + ( + SELECT event_id, user_id, page, load_ms + FROM read_csv('{EXPORTS / f"{ds}.csv.gz"}', header = true, + types = {{'user_id': 'VARCHAR', 'is_bot': 'INTEGER'}}) + WHERE is_bot = 0 AND user_id IS NOT NULL AND user_id <> '' + ) + """ + ).df() + return frame.to_json(orient="records") + + @task + def transform(events_json: str, ds=None) -> str: + import io + + import duckdb + import pandas as pd + + events_df = pd.read_json(io.StringIO(events_json), orient="records", dtype={"user_id": str}) + STAGING.joinpath(ds).mkdir(parents=True, exist_ok=True) + target = STAGING / ds / "page_stats.parquet" + duckdb.sql( + f""" + COPY ( + SELECT page, count(*) AS views, count(DISTINCT user_id) AS unique_users, + round(avg(load_ms), 1) AS avg_load_ms + FROM events_df + GROUP BY page ORDER BY page + ) TO '{target}' (FORMAT parquet) + """ + ) + return str(target) + + @task + def load(stats_path: str, ds=None) -> str: + import duckdb + + out = OUTPUT / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + duckdb.sql( + f"COPY (SELECT * FROM read_parquet('{stats_path}') ORDER BY page) TO '{out}' (HEADER, DELIMITER ',')" + ) + return str(out) + + load(transform(extract())) + + +clickstream_page_stats() diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_records/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_records/dags/clickstream_page_stats.py new file mode 100644 index 0000000..f489485 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_as_xcom_records/dags/clickstream_page_stats.py @@ -0,0 +1,77 @@ +"""Daily page stats from the clickstream export. + +Tasks hand each other file paths (claim checks), never the event data itself: +XCom rows live in the metadata database and must stay small. +""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EXPORTS = PROJECT_ROOT / "data" / "clickstream" +STAGING = PROJECT_ROOT / "staging" / "clickstream_page_stats" +OUTPUT = PROJECT_ROOT / "output" / "page_stats" + + +@dag( + schedule="0 5 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["web-analytics"], +) +def clickstream_page_stats(): + @task + def extract(ds=None) -> list[dict]: + import duckdb + + frame = duckdb.sql( + f""" + ( + SELECT event_id, user_id, page, load_ms + FROM read_csv('{EXPORTS / f"{ds}.csv.gz"}', header = true, + types = {{'user_id': 'VARCHAR', 'is_bot': 'INTEGER'}}) + WHERE is_bot = 0 AND user_id IS NOT NULL AND user_id <> '' + ) + """ + ).df() + return frame.to_dict("records") + + @task + def transform(events: list[dict], ds=None) -> str: + import duckdb + import pandas as pd + + events_df = pd.DataFrame(events) + STAGING.joinpath(ds).mkdir(parents=True, exist_ok=True) + target = STAGING / ds / "page_stats.parquet" + duckdb.sql( + f""" + COPY ( + SELECT page, count(*) AS views, count(DISTINCT user_id) AS unique_users, + round(avg(load_ms), 1) AS avg_load_ms + FROM events_df + GROUP BY page ORDER BY page + ) TO '{target}' (FORMAT parquet) + """ + ) + return str(target) + + @task + def load(stats_path: str, ds=None) -> str: + import duckdb + + out = OUTPUT / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + duckdb.sql( + f"COPY (SELECT * FROM read_parquet('{stats_path}') ORDER BY page) TO '{out}' (HEADER, DELIMITER ',')" + ) + return str(out) + + load(transform(extract())) + + +clickstream_page_stats() diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_via_variable/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_via_variable/dags/clickstream_page_stats.py new file mode 100644 index 0000000..8b417f3 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/events_via_variable/dags/clickstream_page_stats.py @@ -0,0 +1,52 @@ +"""Clickstream page stats; extract stashes the day's events in an Airflow Variable (metadata DB).""" + +from __future__ import annotations + +import csv +import gzip +import json +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import Variable, dag, task + +ROOT = Path(__file__).resolve().parents[1] + + +@dag(schedule="@daily", start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), catchup=False) +def clickstream_page_stats(): + @task + def extract(ds=None) -> str: + key = f"clickstream_events_{ds}" + with gzip.open(ROOT / "data" / "clickstream" / f"{ds}.csv.gz", "rt", newline="") as src: + events = [[r["user_id"], r["page"], int(r["load_ms"])] for r in csv.DictReader(src) + if r["is_bot"] != "1" and r["user_id"].strip()] + Variable.set(key, json.dumps(events)) + return key + + @task + def transform(key: str) -> list[dict]: + views: dict[str, int] = defaultdict(int) + users: dict[str, set] = defaultdict(set) + total: dict[str, int] = defaultdict(int) + for user, page, load_ms in json.loads(Variable.get(key)): + views[page] += 1 + users[page].add(user) + total[page] += load_ms + return [{"page": p, "views": views[p], "unique_users": len(users[p]), + "avg_load_ms": round(total[p] / views[p], 1)} for p in sorted(views)] + + @task + def load(stats: list[dict], ds=None) -> None: + out = ROOT / "output" / "page_stats" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=["page", "views", "unique_users", "avg_load_ms"]) + writer.writeheader() + writer.writerows(stats) + + load(transform(extract())) + + +clickstream_page_stats() diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/mutants/xcom_pull_without_task_ids/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/xcom_pull_without_task_ids/dags/clickstream_page_stats.py new file mode 100644 index 0000000..d6e0dc5 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/mutants/xcom_pull_without_task_ids/dags/clickstream_page_stats.py @@ -0,0 +1,60 @@ +"""Clickstream page stats; claim-check paths pulled Airflow-2 style (no task_ids).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +STAGING = PROJECT_ROOT / "staging" / "clickstream_page_stats" + + +def extract(ds, ti): + import pandas as pd + + events = pd.read_csv(PROJECT_ROOT / "data" / "clickstream" / f"{ds}.csv.gz", dtype={"user_id": "string"}) + events = events[(events["is_bot"] == 0) & events["user_id"].notna()] + path = STAGING / ds / "events.csv" + path.parent.mkdir(parents=True, exist_ok=True) + events.to_csv(path, index=False) + ti.xcom_push(key="events_path", value=str(path)) + + +def transform(ds, ti): + import pandas as pd + + events = pd.read_csv(ti.xcom_pull(key="events_path")) + stats = ( + events.groupby("page") + .agg(views=("page", "size"), unique_users=("user_id", "nunique"), avg_load_ms=("load_ms", "mean")) + .reset_index() + .sort_values("page") + ) + stats["avg_load_ms"] = stats["avg_load_ms"].round(1) + path = STAGING / ds / "stats.csv" + stats.to_csv(path, index=False) + ti.xcom_push(key="stats_path", value=str(path)) + + +def load(ds, ti): + import shutil + + out = PROJECT_ROOT / "output" / "page_stats" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ti.xcom_pull(key="stats_path"), out) + + +with DAG( + dag_id="clickstream_page_stats", + schedule="0 5 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, +): + ( + PythonOperator(task_id="extract", python_callable=extract) + >> PythonOperator(task_id="transform", python_callable=transform) + >> PythonOperator(task_id="load", python_callable=load) + ) diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/reference/dags/clickstream_page_stats.py b/evals/airflow/cases/authoring-clickstream-claim-check/reference/dags/clickstream_page_stats.py new file mode 100644 index 0000000..bb1bc70 --- /dev/null +++ b/evals/airflow/cases/authoring-clickstream-claim-check/reference/dags/clickstream_page_stats.py @@ -0,0 +1,76 @@ +"""Daily page stats from the clickstream export. + +Tasks hand each other file paths (claim checks), never the event data itself: +XCom rows live in the metadata database and must stay small. +""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EXPORTS = PROJECT_ROOT / "data" / "clickstream" +STAGING = PROJECT_ROOT / "staging" / "clickstream_page_stats" +OUTPUT = PROJECT_ROOT / "output" / "page_stats" + + +@dag( + schedule="0 5 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["web-analytics"], +) +def clickstream_page_stats(): + @task + def extract(ds=None) -> str: + import duckdb + + target = STAGING / ds / "events.parquet" + target.parent.mkdir(parents=True, exist_ok=True) + duckdb.sql( + f""" + COPY ( + SELECT event_id, user_id, page, load_ms + FROM read_csv('{EXPORTS / f"{ds}.csv.gz"}', header = true, + types = {{'user_id': 'VARCHAR', 'is_bot': 'INTEGER'}}) + WHERE is_bot = 0 AND user_id IS NOT NULL AND user_id <> '' + ) TO '{target}' (FORMAT parquet) + """ + ) + return str(target) + + @task + def transform(events_path: str, ds=None) -> str: + import duckdb + + target = STAGING / ds / "page_stats.parquet" + duckdb.sql( + f""" + COPY ( + SELECT page, count(*) AS views, count(DISTINCT user_id) AS unique_users, + round(avg(load_ms), 1) AS avg_load_ms + FROM read_parquet('{events_path}') + GROUP BY page ORDER BY page + ) TO '{target}' (FORMAT parquet) + """ + ) + return str(target) + + @task + def load(stats_path: str, ds=None) -> str: + import duckdb + + out = OUTPUT / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + duckdb.sql( + f"COPY (SELECT * FROM read_parquet('{stats_path}') ORDER BY page) TO '{out}' (HEADER, DELIMITER ',')" + ) + return str(out) + + load(transform(extract())) + + +clickstream_page_stats() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/alt_reference/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/alt_reference/dags/daily_revenue.py new file mode 100644 index 0000000..8fe5efe --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/alt_reference/dags/daily_revenue.py @@ -0,0 +1,39 @@ +"""Daily revenue: cron-triggered at 03:00 UTC, reports the day before the trigger.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +ROOT = Path(__file__).resolve().parents[1] + + +@dag(schedule="0 3 * * *", start_date=datetime(2026, 8, 1), catchup=False, tags=["finance"]) +def daily_revenue(): + @task + def report(**context) -> str: + import duckdb + + # Cron schedules trigger at the tick: data_interval_end is the 03:00 trigger time. + trigger = context["data_interval_end"] + day = (trigger - timedelta(days=1)).strftime("%Y-%m-%d") + out = ROOT / "output" / "daily_revenue" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + duckdb.sql( + f""" + COPY ( + SELECT store_id, count(*) AS orders, round(sum(amount), 2) AS revenue + FROM read_csv('{ROOT / "data" / "orders.csv"}', timestampformat = '%Y-%m-%d %H:%M:%S') + WHERE CAST(order_ts AS DATE) = DATE '{day}' + GROUP BY store_id ORDER BY store_id + ) TO '{out}' (HEADER, DELIMITER ',') + """ + ) + return str(out) + + report() + + +daily_revenue() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/case.yaml b/evals/airflow/cases/authoring-daily-revenue-yesterday/case.yaml new file mode 100644 index 0000000..e8a8c42 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/case.yaml @@ -0,0 +1,34 @@ +id: authoring-daily-revenue-yesterday +area: authoring +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1500 +prompt: | + Finance wants a daily revenue file per store. Please add a DAG + `daily_revenue` that runs every day at 03:00 UTC and reports on the previous + calendar day (midnight to midnight UTC) from data/orders.csv. Late orders + trickle in until about 2am, which is why it runs at 3. So the run that + starts at 03:00 on the 29th should report on the 28th. + + Output: output/daily_revenue/.csv, named after the day being + reported, with the columns store_id,orders,revenue (revenue rounded to 2 + decimals), sorted by store_id. Rerunning a day has to produce the same file. + We need to be able to rerun any day since 2026-09-01. +description: | + "Process yesterday's data" in Airflow 3.3 (P2 scheduling semantics). In 3.x + a cron string maps to CronTriggerTimetable, where data_interval_start == + data_interval_end == logical_date == the trigger time, so Airflow-2 habits + (`{{ ds }}`, data_interval_start) silently report the current, incomplete + day. The grader asks the timetable for the scheduled run that fires at + 2026-09-29T03:00Z (and 09-26), executes it with exactly the scheduler's + logical date and data interval, and checks that the run writes only the file + for the previous calendar day with correct totals (boundary orders at 00:04, + 02:58, 03:01 and 23:57 separate a calendar-day filter from a 03:00-03:00 + window). Primary also: daily 03:00 UTC schedule, rerun reproduces the file. + Accepts any timetable (cron string + trigger-time arithmetic, + CronDataIntervalTimetable + data_interval_start, CronTriggerTimetable with + interval=1 day). Mutants: cron string + ds, CronDataIntervalTimetable + + ds minus one day (reports D-2), filter on the 03:00-to-03:00 data interval + instead of the calendar day, wall-clock "yesterday" (pendulum.now()). diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/.gitignore b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/README.md b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/README.md new file mode 100644 index 0000000..ca5be7c --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/README.md @@ -0,0 +1,11 @@ +# retail-finance-dags + +Airflow 3.3 DAGs for the retail finance team. + +- `dags/`: DAG files +- `data/orders.csv`: order export, refreshed continuously. `order_ts` is in UTC. + Stores upload late orders up to about 02:00 UTC the next morning. +- `data/stores.csv`: store dimension +- `output/`: files picked up by the finance reporting tool + +Run a DAG locally with `airflow dags test `. diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/dags/store_dimension.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/dags/store_dimension.py new file mode 100644 index 0000000..82f8fdd --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/dags/store_dimension.py @@ -0,0 +1,31 @@ +"""Publish the store dimension for the reporting tool every Monday.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="0 5 * * 1", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["finance", "dimensions"], +) +def store_dimension(): + @task + def publish() -> str: + out = PROJECT_ROOT / "output" / "stores.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(PROJECT_ROOT / "data" / "stores.csv", out) + return str(out) + + publish() + + +store_dimension() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/orders.csv b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/orders.csv new file mode 100644 index 0000000..792f4ba --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/orders.csv @@ -0,0 +1,330 @@ +order_id,store_id,order_ts,amount +ORD-5001,S02,2026-09-20 00:04:00,174.40 +ORD-5002,S04,2026-09-20 00:06:56,249.40 +ORD-5003,S04,2026-09-20 00:30:50,155.12 +ORD-5004,S01,2026-09-20 02:31:18,239.32 +ORD-5005,S04,2026-09-20 02:58:00,174.40 +ORD-5006,S03,2026-09-20 03:01:00,8.74 +ORD-5007,S01,2026-09-20 05:16:35,33.84 +ORD-5008,S01,2026-09-20 06:41:02,114.92 +ORD-5009,S03,2026-09-20 07:51:53,102.13 +ORD-5010,S02,2026-09-20 08:27:21,49.17 +ORD-5011,S01,2026-09-20 08:35:40,119.08 +ORD-5012,S02,2026-09-20 10:14:24,21.43 +ORD-5013,S02,2026-09-20 13:30:33,54.51 +ORD-5014,S01,2026-09-20 15:13:07,33.25 +ORD-5015,S04,2026-09-20 15:13:12,141.06 +ORD-5016,S04,2026-09-20 15:35:23,52.67 +ORD-5017,S02,2026-09-20 17:16:52,222.23 +ORD-5018,S04,2026-09-20 17:26:36,75.12 +ORD-5019,S04,2026-09-20 18:23:22,126.86 +ORD-5020,S03,2026-09-20 18:39:28,248.73 +ORD-5021,S04,2026-09-20 19:12:26,68.63 +ORD-5022,S01,2026-09-20 21:18:44,42.26 +ORD-5023,S04,2026-09-20 23:07:10,3.70 +ORD-5024,S01,2026-09-20 23:57:00,138.96 +ORD-5025,S02,2026-09-21 00:04:00,240.47 +ORD-5026,S01,2026-09-21 01:20:09,221.14 +ORD-5027,S04,2026-09-21 02:23:24,128.02 +ORD-5028,S02,2026-09-21 02:58:00,234.26 +ORD-5029,S02,2026-09-21 03:01:00,43.09 +ORD-5030,S01,2026-09-21 03:45:36,111.49 +ORD-5031,S04,2026-09-21 04:06:13,194.16 +ORD-5032,S04,2026-09-21 05:49:16,17.19 +ORD-5033,S03,2026-09-21 06:08:30,15.89 +ORD-5034,S03,2026-09-21 06:11:34,165.63 +ORD-5035,S01,2026-09-21 06:20:08,106.63 +ORD-5036,S02,2026-09-21 06:34:41,100.45 +ORD-5037,S03,2026-09-21 08:05:02,162.42 +ORD-5038,S02,2026-09-21 08:47:40,245.91 +ORD-5039,S04,2026-09-21 08:56:52,15.20 +ORD-5040,S02,2026-09-21 11:09:48,239.84 +ORD-5041,S04,2026-09-21 11:36:04,9.99 +ORD-5042,S02,2026-09-21 12:06:05,225.16 +ORD-5043,S03,2026-09-21 12:37:11,199.42 +ORD-5044,S03,2026-09-21 12:40:40,151.53 +ORD-5045,S04,2026-09-21 13:38:55,122.66 +ORD-5046,S04,2026-09-21 14:49:08,79.07 +ORD-5047,S03,2026-09-21 15:36:32,37.41 +ORD-5048,S01,2026-09-21 18:55:13,97.44 +ORD-5049,S02,2026-09-21 19:00:42,238.63 +ORD-5050,S01,2026-09-21 20:15:39,68.31 +ORD-5051,S03,2026-09-21 20:48:30,162.03 +ORD-5052,S04,2026-09-21 21:44:40,66.53 +ORD-5053,S04,2026-09-21 22:10:09,188.65 +ORD-5054,S02,2026-09-21 22:40:08,35.98 +ORD-5055,S01,2026-09-21 22:48:36,101.05 +ORD-5056,S02,2026-09-21 23:57:00,9.25 +ORD-5057,S04,2026-09-21 23:57:22,197.18 +ORD-5058,S01,2026-09-22 00:04:00,198.31 +ORD-5059,S01,2026-09-22 01:40:53,98.75 +ORD-5060,S01,2026-09-22 02:58:00,101.29 +ORD-5061,S02,2026-09-22 03:01:00,214.05 +ORD-5062,S02,2026-09-22 04:20:07,65.47 +ORD-5063,S03,2026-09-22 04:36:24,187.21 +ORD-5064,S02,2026-09-22 06:02:48,99.05 +ORD-5065,S03,2026-09-22 06:36:39,182.36 +ORD-5066,S03,2026-09-22 06:41:49,204.95 +ORD-5067,S04,2026-09-22 07:20:26,237.21 +ORD-5068,S01,2026-09-22 07:59:44,137.72 +ORD-5069,S01,2026-09-22 10:14:35,128.53 +ORD-5070,S03,2026-09-22 11:04:09,241.88 +ORD-5071,S03,2026-09-22 11:48:23,75.03 +ORD-5072,S02,2026-09-22 13:45:20,18.74 +ORD-5073,S03,2026-09-22 14:25:41,244.28 +ORD-5074,S03,2026-09-22 17:58:52,19.13 +ORD-5075,S04,2026-09-22 19:36:43,29.58 +ORD-5076,S01,2026-09-22 20:01:34,163.97 +ORD-5077,S04,2026-09-22 21:59:14,127.75 +ORD-5078,S01,2026-09-22 22:37:02,170.93 +ORD-5079,S04,2026-09-22 23:51:34,119.31 +ORD-5080,S04,2026-09-22 23:57:00,219.29 +ORD-5081,S01,2026-09-23 00:04:00,12.25 +ORD-5082,S02,2026-09-23 00:54:30,124.50 +ORD-5083,S02,2026-09-23 01:31:50,195.54 +ORD-5084,S03,2026-09-23 01:46:38,63.59 +ORD-5085,S04,2026-09-23 02:58:00,98.33 +ORD-5086,S04,2026-09-23 03:01:00,206.73 +ORD-5087,S01,2026-09-23 08:12:34,42.98 +ORD-5088,S02,2026-09-23 08:17:36,80.00 +ORD-5089,S04,2026-09-23 08:36:30,179.96 +ORD-5090,S01,2026-09-23 08:59:42,139.65 +ORD-5091,S01,2026-09-23 10:02:01,132.03 +ORD-5092,S04,2026-09-23 11:55:25,186.36 +ORD-5093,S04,2026-09-23 14:01:56,245.80 +ORD-5094,S02,2026-09-23 14:31:29,176.88 +ORD-5095,S02,2026-09-23 14:43:49,170.63 +ORD-5096,S03,2026-09-23 15:32:53,161.47 +ORD-5097,S02,2026-09-23 15:47:54,67.43 +ORD-5098,S01,2026-09-23 16:36:07,82.56 +ORD-5099,S03,2026-09-23 17:43:23,236.40 +ORD-5100,S02,2026-09-23 21:27:34,225.23 +ORD-5101,S02,2026-09-23 23:00:14,61.88 +ORD-5102,S03,2026-09-23 23:49:24,179.90 +ORD-5103,S02,2026-09-23 23:57:00,235.36 +ORD-5104,S02,2026-09-24 00:04:00,247.73 +ORD-5105,S02,2026-09-24 00:41:28,26.35 +ORD-5106,S03,2026-09-24 01:15:05,50.00 +ORD-5107,S03,2026-09-24 01:25:47,223.93 +ORD-5108,S03,2026-09-24 02:43:34,44.05 +ORD-5109,S04,2026-09-24 02:58:00,133.77 +ORD-5110,S01,2026-09-24 03:01:00,79.24 +ORD-5111,S04,2026-09-24 03:36:10,102.53 +ORD-5112,S02,2026-09-24 06:20:51,58.60 +ORD-5113,S04,2026-09-24 06:51:41,221.24 +ORD-5114,S01,2026-09-24 07:50:05,165.70 +ORD-5115,S01,2026-09-24 09:55:04,22.87 +ORD-5116,S02,2026-09-24 10:13:02,40.57 +ORD-5117,S04,2026-09-24 12:45:53,204.36 +ORD-5118,S04,2026-09-24 13:23:13,119.36 +ORD-5119,S03,2026-09-24 14:00:14,40.72 +ORD-5120,S02,2026-09-24 14:29:46,90.22 +ORD-5121,S03,2026-09-24 14:37:29,161.07 +ORD-5122,S01,2026-09-24 15:54:22,222.87 +ORD-5123,S01,2026-09-24 16:03:53,168.04 +ORD-5124,S02,2026-09-24 16:22:14,129.40 +ORD-5125,S02,2026-09-24 16:46:27,91.97 +ORD-5126,S03,2026-09-24 18:14:53,17.51 +ORD-5127,S01,2026-09-24 21:07:04,147.56 +ORD-5128,S03,2026-09-24 22:07:53,171.15 +ORD-5129,S04,2026-09-24 22:28:03,42.07 +ORD-5130,S01,2026-09-24 22:38:08,60.47 +ORD-5131,S03,2026-09-24 22:55:04,136.02 +ORD-5132,S04,2026-09-24 23:57:00,52.15 +ORD-5133,S01,2026-09-25 00:04:00,226.87 +ORD-5134,S01,2026-09-25 00:45:58,56.68 +ORD-5135,S01,2026-09-25 01:28:25,242.63 +ORD-5136,S04,2026-09-25 02:51:04,54.91 +ORD-5137,S04,2026-09-25 02:58:00,234.38 +ORD-5138,S03,2026-09-25 03:01:00,5.78 +ORD-5139,S03,2026-09-25 05:53:55,45.82 +ORD-5140,S03,2026-09-25 06:20:18,152.98 +ORD-5141,S04,2026-09-25 07:46:52,162.77 +ORD-5142,S02,2026-09-25 07:58:33,48.04 +ORD-5143,S04,2026-09-25 11:49:05,54.09 +ORD-5144,S03,2026-09-25 12:49:33,194.57 +ORD-5145,S03,2026-09-25 13:24:59,227.81 +ORD-5146,S02,2026-09-25 15:25:05,103.18 +ORD-5147,S02,2026-09-25 16:04:26,89.92 +ORD-5148,S03,2026-09-25 17:25:16,144.51 +ORD-5149,S01,2026-09-25 17:58:44,228.19 +ORD-5150,S01,2026-09-25 18:18:19,246.12 +ORD-5151,S02,2026-09-25 19:05:57,186.21 +ORD-5152,S02,2026-09-25 19:23:22,232.14 +ORD-5153,S03,2026-09-25 22:05:18,224.08 +ORD-5154,S03,2026-09-25 22:52:22,33.34 +ORD-5155,S02,2026-09-25 23:02:49,166.56 +ORD-5156,S04,2026-09-25 23:23:44,193.41 +ORD-5157,S02,2026-09-25 23:57:00,163.74 +ORD-5158,S01,2026-09-26 00:04:00,3.93 +ORD-5159,S03,2026-09-26 01:17:39,145.38 +ORD-5160,S04,2026-09-26 01:28:29,51.64 +ORD-5161,S02,2026-09-26 02:58:00,217.86 +ORD-5162,S04,2026-09-26 03:01:00,169.19 +ORD-5163,S01,2026-09-26 05:04:26,94.49 +ORD-5164,S02,2026-09-26 05:10:50,126.46 +ORD-5165,S04,2026-09-26 05:46:16,90.51 +ORD-5166,S04,2026-09-26 06:42:48,150.83 +ORD-5167,S01,2026-09-26 07:29:36,216.72 +ORD-5168,S01,2026-09-26 07:58:17,95.60 +ORD-5169,S03,2026-09-26 08:05:32,184.36 +ORD-5170,S04,2026-09-26 09:39:57,198.85 +ORD-5171,S02,2026-09-26 10:59:32,150.06 +ORD-5172,S03,2026-09-26 12:14:13,222.98 +ORD-5173,S01,2026-09-26 13:43:17,187.26 +ORD-5174,S02,2026-09-26 13:45:50,159.52 +ORD-5175,S02,2026-09-26 14:19:51,188.67 +ORD-5176,S03,2026-09-26 15:39:37,28.74 +ORD-5177,S03,2026-09-26 16:01:51,162.56 +ORD-5178,S02,2026-09-26 16:42:13,36.08 +ORD-5179,S04,2026-09-26 17:22:27,56.81 +ORD-5180,S03,2026-09-26 17:49:29,230.59 +ORD-5181,S03,2026-09-26 19:34:52,5.27 +ORD-5182,S02,2026-09-26 21:21:06,73.08 +ORD-5183,S03,2026-09-26 23:57:00,175.00 +ORD-5184,S04,2026-09-27 00:04:00,180.59 +ORD-5185,S02,2026-09-27 00:46:12,111.46 +ORD-5186,S02,2026-09-27 00:46:31,7.91 +ORD-5187,S04,2026-09-27 02:12:01,211.36 +ORD-5188,S01,2026-09-27 02:58:00,88.18 +ORD-5189,S02,2026-09-27 03:01:00,43.77 +ORD-5190,S03,2026-09-27 04:29:13,83.75 +ORD-5191,S03,2026-09-27 05:11:34,76.92 +ORD-5192,S01,2026-09-27 08:27:51,115.82 +ORD-5193,S02,2026-09-27 12:36:40,40.46 +ORD-5194,S02,2026-09-27 13:18:56,212.29 +ORD-5195,S02,2026-09-27 13:36:47,139.05 +ORD-5196,S02,2026-09-27 14:37:48,135.01 +ORD-5197,S01,2026-09-27 15:44:50,158.25 +ORD-5198,S01,2026-09-27 16:09:16,190.93 +ORD-5199,S02,2026-09-27 16:09:23,176.01 +ORD-5200,S02,2026-09-27 17:45:52,136.06 +ORD-5201,S01,2026-09-27 18:13:38,195.64 +ORD-5202,S02,2026-09-27 18:36:14,151.05 +ORD-5203,S01,2026-09-27 19:34:14,63.45 +ORD-5204,S04,2026-09-27 19:58:50,145.45 +ORD-5205,S01,2026-09-27 21:39:01,101.72 +ORD-5206,S03,2026-09-27 21:54:47,202.20 +ORD-5207,S03,2026-09-27 23:08:45,221.47 +ORD-5208,S04,2026-09-27 23:08:50,11.48 +ORD-5209,S04,2026-09-27 23:09:49,14.46 +ORD-5210,S04,2026-09-27 23:50:25,193.51 +ORD-5211,S04,2026-09-27 23:57:00,47.99 +ORD-5212,S04,2026-09-28 00:04:00,226.92 +ORD-5213,S01,2026-09-28 01:56:41,95.42 +ORD-5214,S04,2026-09-28 02:54:18,129.63 +ORD-5215,S01,2026-09-28 02:58:00,158.04 +ORD-5216,S04,2026-09-28 03:01:00,56.84 +ORD-5217,S03,2026-09-28 03:17:34,182.24 +ORD-5218,S01,2026-09-28 04:04:55,125.12 +ORD-5219,S03,2026-09-28 04:12:10,153.23 +ORD-5220,S04,2026-09-28 05:01:27,49.08 +ORD-5221,S01,2026-09-28 05:26:18,108.10 +ORD-5222,S02,2026-09-28 05:58:12,43.19 +ORD-5223,S02,2026-09-28 09:21:13,55.92 +ORD-5224,S03,2026-09-28 09:31:55,6.25 +ORD-5225,S02,2026-09-28 09:36:55,213.00 +ORD-5226,S04,2026-09-28 09:49:06,64.43 +ORD-5227,S03,2026-09-28 10:27:31,133.53 +ORD-5228,S04,2026-09-28 10:31:34,49.88 +ORD-5229,S02,2026-09-28 10:40:07,173.82 +ORD-5230,S04,2026-09-28 11:13:41,38.40 +ORD-5231,S03,2026-09-28 11:21:06,223.18 +ORD-5232,S02,2026-09-28 11:49:34,74.08 +ORD-5233,S01,2026-09-28 12:24:37,220.11 +ORD-5234,S02,2026-09-28 13:30:38,180.61 +ORD-5235,S03,2026-09-28 14:10:28,83.93 +ORD-5236,S04,2026-09-28 14:12:59,205.82 +ORD-5237,S02,2026-09-28 17:10:34,181.22 +ORD-5238,S01,2026-09-28 17:43:18,157.60 +ORD-5239,S03,2026-09-28 17:52:33,229.32 +ORD-5240,S02,2026-09-28 19:19:10,199.17 +ORD-5241,S02,2026-09-28 21:17:40,198.11 +ORD-5242,S01,2026-09-28 23:53:00,223.72 +ORD-5243,S04,2026-09-28 23:57:00,244.68 +ORD-5244,S03,2026-09-29 00:04:00,164.23 +ORD-5245,S03,2026-09-29 00:08:46,105.11 +ORD-5246,S04,2026-09-29 01:45:44,237.89 +ORD-5247,S01,2026-09-29 01:45:59,71.98 +ORD-5248,S01,2026-09-29 02:42:43,176.35 +ORD-5249,S03,2026-09-29 02:58:00,117.43 +ORD-5250,S03,2026-09-29 03:01:00,220.71 +ORD-5251,S03,2026-09-29 03:22:08,212.01 +ORD-5252,S02,2026-09-29 04:02:25,133.89 +ORD-5253,S03,2026-09-29 04:03:11,53.59 +ORD-5254,S02,2026-09-29 04:42:51,205.17 +ORD-5255,S03,2026-09-29 04:59:01,199.22 +ORD-5256,S02,2026-09-29 06:02:31,3.90 +ORD-5257,S03,2026-09-29 06:04:21,13.67 +ORD-5258,S03,2026-09-29 06:25:48,174.25 +ORD-5259,S01,2026-09-29 07:42:19,13.42 +ORD-5260,S01,2026-09-29 10:44:10,45.85 +ORD-5261,S02,2026-09-29 11:56:00,212.82 +ORD-5262,S03,2026-09-29 13:51:48,156.55 +ORD-5263,S02,2026-09-29 14:03:53,153.01 +ORD-5264,S02,2026-09-29 15:40:36,27.08 +ORD-5265,S03,2026-09-29 16:32:32,104.85 +ORD-5266,S03,2026-09-29 17:14:06,56.50 +ORD-5267,S04,2026-09-29 18:42:33,97.86 +ORD-5268,S03,2026-09-29 18:54:21,187.28 +ORD-5269,S01,2026-09-29 23:10:52,63.29 +ORD-5270,S03,2026-09-29 23:41:57,59.24 +ORD-5271,S04,2026-09-29 23:50:56,193.14 +ORD-5272,S02,2026-09-29 23:57:00,249.00 +ORD-5273,S03,2026-09-30 00:04:00,131.69 +ORD-5274,S01,2026-09-30 00:19:25,158.60 +ORD-5275,S01,2026-09-30 01:08:02,218.26 +ORD-5276,S04,2026-09-30 01:49:38,25.23 +ORD-5277,S04,2026-09-30 02:58:00,92.77 +ORD-5278,S04,2026-09-30 03:01:00,197.62 +ORD-5279,S04,2026-09-30 03:05:06,188.19 +ORD-5280,S02,2026-09-30 04:23:27,142.73 +ORD-5281,S02,2026-09-30 06:04:34,77.55 +ORD-5282,S02,2026-09-30 06:40:34,42.40 +ORD-5283,S04,2026-09-30 06:48:12,164.48 +ORD-5284,S03,2026-09-30 13:51:46,192.96 +ORD-5285,S04,2026-09-30 15:57:23,135.72 +ORD-5286,S01,2026-09-30 16:46:53,93.94 +ORD-5287,S01,2026-09-30 16:53:50,228.12 +ORD-5288,S02,2026-09-30 17:42:49,222.46 +ORD-5289,S04,2026-09-30 17:43:41,87.36 +ORD-5290,S03,2026-09-30 19:58:50,130.36 +ORD-5291,S02,2026-09-30 20:13:26,165.53 +ORD-5292,S04,2026-09-30 22:25:19,72.05 +ORD-5293,S02,2026-09-30 22:26:19,172.41 +ORD-5294,S03,2026-09-30 22:29:50,81.24 +ORD-5295,S04,2026-09-30 23:57:00,134.98 +ORD-5296,S01,2026-10-01 00:04:00,129.09 +ORD-5297,S02,2026-10-01 02:16:40,161.20 +ORD-5298,S04,2026-10-01 02:58:00,249.15 +ORD-5299,S04,2026-10-01 03:01:00,67.46 +ORD-5300,S02,2026-10-01 03:10:03,109.52 +ORD-5301,S01,2026-10-01 03:26:52,178.70 +ORD-5302,S04,2026-10-01 04:12:49,106.21 +ORD-5303,S03,2026-10-01 06:01:19,233.09 +ORD-5304,S04,2026-10-01 06:11:57,124.44 +ORD-5305,S02,2026-10-01 06:32:28,17.74 +ORD-5306,S03,2026-10-01 06:45:18,65.21 +ORD-5307,S03,2026-10-01 07:23:58,133.81 +ORD-5308,S01,2026-10-01 07:24:46,68.13 +ORD-5309,S02,2026-10-01 08:21:44,239.33 +ORD-5310,S02,2026-10-01 09:02:54,216.33 +ORD-5311,S01,2026-10-01 09:21:30,85.66 +ORD-5312,S02,2026-10-01 09:36:58,45.27 +ORD-5313,S02,2026-10-01 09:39:42,80.87 +ORD-5314,S01,2026-10-01 09:50:40,11.35 +ORD-5315,S01,2026-10-01 10:40:17,212.45 +ORD-5316,S04,2026-10-01 15:25:06,207.08 +ORD-5317,S02,2026-10-01 17:30:51,44.64 +ORD-5318,S04,2026-10-01 18:16:40,96.10 +ORD-5319,S03,2026-10-01 19:13:38,117.55 +ORD-5320,S01,2026-10-01 19:28:47,245.42 +ORD-5321,S02,2026-10-01 19:34:52,48.99 +ORD-5322,S04,2026-10-01 19:41:23,162.02 +ORD-5323,S04,2026-10-01 20:30:14,43.53 +ORD-5324,S04,2026-10-01 21:43:36,87.69 +ORD-5325,S02,2026-10-01 22:01:36,162.18 +ORD-5326,S02,2026-10-01 22:53:30,27.52 +ORD-5327,S03,2026-10-01 23:02:21,12.78 +ORD-5328,S04,2026-10-01 23:25:21,200.75 +ORD-5329,S03,2026-10-01 23:57:00,138.63 diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/stores.csv b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/stores.csv new file mode 100644 index 0000000..5549c0f --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/data/stores.csv @@ -0,0 +1,5 @@ +store_id,store_name,city +S01,Downtown,Chicago +S02,Lakeside,Chicago +S03,Harbor,Boston +S04,Airport,Denver diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/requirements.txt b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/requirements.txt new file mode 100644 index 0000000..239e2e6 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +pandas==3.0.5 +duckdb==1.5.6 diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/grade.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/grade.py new file mode 100644 index 0000000..4ce4539 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/grade.py @@ -0,0 +1,159 @@ +"""Grader for authoring-daily-revenue-yesterday (Airflow 3.3). + +The grader reproduces the scheduler: it asks the DAG's timetable for the +scheduled run that fires at 03:00 UTC on day D, then executes that run with +exactly the scheduler's logical date and data interval (``dag.test`` with the +timetable's manual-interval inference pinned to the scheduled interval). The +run must write only the file for D-1, with that calendar day's orders. +Works for any timetable choice (cron string, CronDataIntervalTimetable, +CronTriggerTimetable with an interval, ...). +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "daily_revenue" +FIXTURE_DATA = CASE_DIR / "fixture" / "data" +# scheduled trigger time -> calendar day that run must report +SCENARIOS = {"2026-09-29T03:00:00+00:00": "2026-09-28", "2026-09-26T03:00:00+00:00": "2026-09-25"} +TOL = 0.011 + +SCHEDULED_RUN_PROBE = r''' +import pendulum +from airflow.timetables.base import DataInterval +DAG_ID, RUN_AFTER = __ARGS__ +dag = get_dag(DAG_ID) +target = pendulum.parse(RUN_AFTER) +runs = scheduled_intervals(dag, target.subtract(days=2), n=4) +info = next((r for r in runs if pendulum.parse(r["run_after"]) == target), None) +if info is None: + RESULT = {"error": f"the timetable has no run firing at {RUN_AFTER}", "runs": runs} +else: + start, end = pendulum.parse(info["start"]), pendulum.parse(info["end"]) + # dag.test() derives the interval via infer_manual_data_interval(); pin it + # to the scheduled interval so the run matches what the scheduler creates. + cls = type(core_timetable(dag)) + cls.infer_manual_data_interval = lambda self, *, run_after: DataInterval(start, end) + dr = dag.test(logical_date=start, run_after=target) + from airflow.models.taskinstance import TaskInstance + from airflow.utils.session import create_session + with create_session() as s: + tis = {ti.task_id: str(getattr(ti.state, "value", ti.state)) + for ti in s.query(TaskInstance).filter(TaskInstance.dag_id == DAG_ID, + TaskInstance.run_id == dr.run_id)} + RESULT = {"info": info, "state": str(getattr(dr.state, "value", dr.state)), "tasks": tis, + "logical_date": dr.logical_date.isoformat() if dr.logical_date else None} +''' + + +def expected(day: str) -> dict[str, tuple[int, float]]: + orders: dict[str, int] = defaultdict(int) + revenue: dict[str, float] = defaultdict(float) + with (FIXTURE_DATA / "orders.csv").open(newline="") as fh: + for r in csv.DictReader(fh): + if r["order_ts"][:10] == day: + orders[r["store_id"]] += 1 + revenue[r["store_id"]] += float(r["amount"]) + return {s: (orders[s], round(revenue[s], 2)) for s in orders} + + +def read_output(path: Path) -> dict[str, tuple[int, float]] | str: + if not path.exists(): + return "missing" + try: + with path.open(newline="") as fh: + return {r["store_id"].strip(): (int(float(r["orders"])), round(float(r["revenue"]), 2)) + for r in csv.DictReader(fh)} + except (KeyError, ValueError) as exc: + return f"unreadable: {exc!r}" + + +def matches(got, want: dict[str, tuple[int, float]]) -> bool: + return isinstance(got, dict) and set(got) == set(want) and all( + got[s][0] == want[s][0] and abs(got[s][1] - want[s][1]) <= TOL for s in want) + + +def run_scheduled(py: str, ws: Path, env: dict, run_after: str) -> tuple[dict | None, str]: + code = SCHEDULED_RUN_PROBE.replace("__ARGS__", repr((DAG_ID, run_after))) + res, proc = g.probe_json(py, code, ws, env=env) + return res, proc.tail(40) + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + shutil.rmtree(ws / "output", ignore_errors=True) + shutil.rmtree(ws / "data", ignore_errors=True) + shutil.copytree(FIXTURE_DATA, ws / "data") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is None: + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + return + + res, proc = g.probe_json(py, f''' + RESULT = scheduled_intervals(get_dag({DAG_ID!r}), "2026-10-01T00:00:00+00:00", n=3) + ''', ws) + stamps = [datetime.fromisoformat(r["run_after"]) for r in (res or [])] + daily_0300 = len(stamps) == 3 and all( + s.utcoffset().total_seconds() == 0 and (s.hour, s.minute) == (3, 0) for s in stamps) and all( + (b - a).total_seconds() == 86400 for a, b in zip(stamps, stamps[1:])) + grader.primary("scheduled runs fire daily at 03:00 UTC", daily_0300, + json.dumps(res) if res is not None else proc.tail(20)) + + env = g.airflow_env(ws) + g.ensure_db(py, env) + out_dir = ws / "output" / "daily_revenue" + first_after, first_day = next(iter(SCENARIOS.items())) + runs_ok, detail, reported_ok = True, [], True + for i, (run_after, day) in enumerate(SCENARIOS.items()): + before = set(p.name for p in out_dir.glob("*.csv")) if out_dir.exists() else set() + res, tail = run_scheduled(py, ws, env, run_after) + ok = bool(res) and res.get("state") == "success" and bool(res.get("tasks")) + runs_ok &= ok + written = (set(p.name for p in out_dir.glob("*.csv")) if out_dir.exists() else set()) - before + got = read_output(out_dir / f"{day}.csv") + good = ok and written == {f"{day}.csv"} and matches(got, expected(day)) + reported_ok &= good + detail.append(f"run firing {run_after}: state={res.get('state') if res else None} " + f"logical_date={res.get('logical_date') if res else None} new files={sorted(written)} " + f"expected {day}.csv={expected(day)} got={got}" + + ("" if res else f"\nprobe output: {tail}") + + (f" error={res.get('error')} runs={res.get('runs')}" if res and res.get("error") else "")) + grader.primary("scheduled runs execute successfully", runs_ok, "\n".join(detail)) + grader.primary("the 03:00 run on day D reports calendar day D-1 (and only that day)", reported_ok, + "\n".join(detail)) + + res, tail = run_scheduled(py, ws, env, first_after) + again = read_output(out_dir / f"{first_day}.csv") + grader.primary("rerun of a scheduled run reproduces the same file", + bool(res) and res.get("state") == "success" and matches(again, expected(first_day)), + f"state={res.get('state') if res else tail}; got={again}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/cron_string_uses_ds/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/cron_string_uses_ds/dags/daily_revenue.py new file mode 100644 index 0000000..7e081a2 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/cron_string_uses_ds/dags/daily_revenue.py @@ -0,0 +1,49 @@ +"""Daily revenue per store for the previous UTC calendar day. + +The schedule is an explicit CronDataIntervalTimetable: the run that starts at +03:00 UTC on day D covers the interval [D-1 03:00, D 03:00), so +`data_interval_start` falls on D-1, the day being reported. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["finance"], +) +def daily_revenue(): + @task + def aggregate(ds=None) -> str: + day = ds + orders: dict[str, int] = defaultdict(int) + revenue: dict[str, float] = defaultdict(float) + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["order_ts"][:10] == day: + orders[row["store_id"]] += 1 + revenue[row["store_id"]] += float(row["amount"]) + out = PROJECT_ROOT / "output" / "daily_revenue" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["store_id", "orders", "revenue"]) + for store in sorted(orders): + writer.writerow([store, orders[store], f"{revenue[store]:.2f}"]) + return str(out) + + aggregate() + + +daily_revenue() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/filters_on_data_interval_window/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/filters_on_data_interval_window/dags/daily_revenue.py new file mode 100644 index 0000000..0b2b62f --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/filters_on_data_interval_window/dags/daily_revenue.py @@ -0,0 +1,51 @@ +"""Daily revenue per store for the previous UTC calendar day. + +The schedule is an explicit CronDataIntervalTimetable: the run that starts at +03:00 UTC on day D covers the interval [D-1 03:00, D 03:00), so +`data_interval_start` falls on D-1, the day being reported. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["finance"], +) +def daily_revenue(): + @task + def aggregate(data_interval_start=None, data_interval_end=None) -> str: + lo = data_interval_start.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + day = data_interval_start.in_timezone("UTC").date().isoformat() + orders: dict[str, int] = defaultdict(int) + revenue: dict[str, float] = defaultdict(float) + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if lo <= row["order_ts"] < hi: + orders[row["store_id"]] += 1 + revenue[row["store_id"]] += float(row["amount"]) + out = PROJECT_ROOT / "output" / "daily_revenue" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["store_id", "orders", "revenue"]) + for store in sorted(orders): + writer.writerow([store, orders[store], f"{revenue[store]:.2f}"]) + return str(out) + + aggregate() + + +daily_revenue() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/interval_timetable_ds_minus_one/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/interval_timetable_ds_minus_one/dags/daily_revenue.py new file mode 100644 index 0000000..5eec1e9 --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/interval_timetable_ds_minus_one/dags/daily_revenue.py @@ -0,0 +1,49 @@ +"""Daily revenue per store for the previous UTC calendar day. + +The schedule is an explicit CronDataIntervalTimetable: the run that starts at +03:00 UTC on day D covers the interval [D-1 03:00, D 03:00), so +`data_interval_start` falls on D-1, the day being reported. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["finance"], +) +def daily_revenue(): + @task + def aggregate(ds=None) -> str: + day = pendulum.parse(ds).subtract(days=1).date().isoformat() + orders: dict[str, int] = defaultdict(int) + revenue: dict[str, float] = defaultdict(float) + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["order_ts"][:10] == day: + orders[row["store_id"]] += 1 + revenue[row["store_id"]] += float(row["amount"]) + out = PROJECT_ROOT / "output" / "daily_revenue" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["store_id", "orders", "revenue"]) + for store in sorted(orders): + writer.writerow([store, orders[store], f"{revenue[store]:.2f}"]) + return str(out) + + aggregate() + + +daily_revenue() diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/wall_clock_yesterday/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/wall_clock_yesterday/dags/daily_revenue.py new file mode 100644 index 0000000..0f075de --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/mutants/wall_clock_yesterday/dags/daily_revenue.py @@ -0,0 +1,46 @@ +"""Daily revenue per store for the previous UTC calendar day (classic operator, templated date).""" + +from __future__ import annotations + +import csv +from collections import Counter +from decimal import Decimal +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +ROOT = Path(__file__).resolve().parents[1] + + +def build_report(report_day: str) -> str: + orders: Counter[str] = Counter() + revenue: dict[str, Decimal] = {} + with open(ROOT / "data" / "orders.csv", newline="") as fh: + for row in csv.DictReader(fh): + if pendulum.parse(row["order_ts"], tz="UTC").to_date_string() != report_day: + continue + orders[row["store_id"]] += 1 + revenue[row["store_id"]] = revenue.get(row["store_id"], Decimal(0)) + Decimal(row["amount"]) + out = ROOT / "output" / "daily_revenue" / f"{report_day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with open(out, "w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["store_id", "orders", "revenue"]) + for store in sorted(orders): + w.writerow([store, orders[store], str(revenue[store].quantize(Decimal("0.01")))]) + return str(out) + + +with DAG( + dag_id="daily_revenue", + schedule="0 3 * * *", # 3.x: CronTriggerTimetable, logical date == trigger time + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, +): + PythonOperator( + task_id="build_report", + python_callable=build_report, + op_kwargs={"report_day": pendulum.now("UTC").subtract(days=1).to_date_string()}, + ) diff --git a/evals/airflow/cases/authoring-daily-revenue-yesterday/reference/dags/daily_revenue.py b/evals/airflow/cases/authoring-daily-revenue-yesterday/reference/dags/daily_revenue.py new file mode 100644 index 0000000..ebd7a7d --- /dev/null +++ b/evals/airflow/cases/authoring-daily-revenue-yesterday/reference/dags/daily_revenue.py @@ -0,0 +1,49 @@ +"""Daily revenue per store for the previous UTC calendar day. + +The schedule is an explicit CronDataIntervalTimetable: the run that starts at +03:00 UTC on day D covers the interval [D-1 03:00, D 03:00), so +`data_interval_start` falls on D-1, the day being reported. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["finance"], +) +def daily_revenue(): + @task + def aggregate(data_interval_start=None) -> str: + day = data_interval_start.in_timezone("UTC").date().isoformat() + orders: dict[str, int] = defaultdict(int) + revenue: dict[str, float] = defaultdict(float) + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["order_ts"][:10] == day: + orders[row["store_id"]] += 1 + revenue[row["store_id"]] += float(row["amount"]) + out = PROJECT_ROOT / "output" / "daily_revenue" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["store_id", "orders", "revenue"]) + for store in sorted(orders): + writer.writerow([store, orders[store], f"{revenue[store]:.2f}"]) + return str(out) + + aggregate() + + +daily_revenue() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/alt_reference/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/alt_reference/dags/partner_feeds.py new file mode 100644 index 0000000..5f22175 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/alt_reference/dags/partner_feeds.py @@ -0,0 +1,57 @@ +"""Partner feeds with a classic PythonOperator mapped via expand(op_kwargs=...).""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, task + +BASE = Path(__file__).resolve().parents[1] + + +def load_partner_file(path: str, ds: str) -> None: + import pandas as pd + + frame = pd.read_csv(path) + partner = Path(path).stem + out_dir = BASE / "output" / "partner_feeds" / ds + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / f"{partner}.json").write_text(json.dumps({ + "partner": partner, + "rows": int(len(frame)), + "total_amount": float(round(frame["amount"].sum(), 2)), + })) + + +with DAG( + dag_id="partner_feeds", + schedule="@daily", + start_date=datetime(2026, 8, 1), + catchup=False, + max_active_runs=1, +): + + @task + def find_files(ds=None) -> list[dict]: + folder = BASE / "data" / "partner_drop" / ds + return [{"path": str(p), "ds": ds} for p in sorted(folder.glob("*.csv"))] + + per_file = PythonOperator.partial(task_id="load_partner_file", python_callable=load_partner_file).expand( + op_kwargs=find_files() + ) + + @task(trigger_rule="none_failed") + def summary(ds=None) -> None: + out_dir = BASE / "output" / "partner_feeds" / ds + out_dir.mkdir(parents=True, exist_ok=True) + parts = [json.loads(p.read_text()) for p in sorted(out_dir.glob("*.json")) if p.name != "summary.json"] + (out_dir / "summary.json").write_text(json.dumps({ + "files": len(parts), + "rows": sum(p["rows"] for p in parts), + "total_amount": round(sum(p["total_amount"] for p in parts), 2), + })) + + per_file >> summary() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/case.yaml b/evals/airflow/cases/authoring-partner-feeds-mapping/case.yaml new file mode 100644 index 0000000..2f7ef63 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/case.yaml @@ -0,0 +1,41 @@ +id: authoring-partner-feeds-mapping +area: authoring +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1500 +prompt: | + Partners upload their daily order files to + data/partner_drop//.csv (columns order_id,amount). The + set of partners changes all the time, and on some days nobody sends anything, + in which case that date's folder doesn't exist at all. + + I need a daily DAG `partner_feeds` that handles the files for the run's date. + Every file has to be processed as its own task instance, so a broken file + only fails (and retries) its own work and doesn't hold up the others. For + each file, write output/partner_feeds//.json containing + {"partner": ..., "rows": , "total_amount": }. + + Once every file is processed, write output/partner_feeds//summary.json + with {"files": , "rows": , "total_amount": }. + The summary has to be written even on days with no files (all zeros), + because finance checks it every morning. + + We need to be able to rerun any date since 2026-09-01. +description: | + Dynamic task mapping over a list produced at runtime (P9) without parse-time + file listing (P3). Primary: DAG imports; task ids are identical when parsed + with the sample files, with no partner_drop dir, and with extra files (so no + parse-time loop); at least one mapped task; `airflow dags test` succeeds for + days with 0, 1 and 4 files and some mapped task expands to exactly 0/1/4 + instances; per-partner JSONs and summary.json are correct (the summary must + exist on the empty day, which needs a trigger rule or equivalent because an + empty expansion skips the mapped task); a rerun reproduces the outputs. + The grader replaces data/partner_drop with grader_data/partner_drop (different + rows, and a partner missing from data/partners.json). A mapped task group + counts as mapping. + Mutants: one task per file generated by a parse-time loop, a single task that + loops over all files, a summary with the default trigger rule (skipped on + empty days), a lister that fails when the date folder is missing, mapping over + the partner registry instead of the files. diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/.gitignore b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/README.md b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/README.md new file mode 100644 index 0000000..40ae439 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/README.md @@ -0,0 +1,11 @@ +# partner-integrations + +Airflow 3.3 DAGs that ingest data sent to us by marketplace partners. + +- `dags/`: DAG files +- `data/partner_drop//.csv`: files partners upload over SFTP, + one folder per business date (sample days included for local testing) +- `data/partners.json`: partner registry +- `output/`: DAG outputs + +Run a DAG locally with `airflow dags test `. diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/dags/partner_registry_export.py b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/dags/partner_registry_export.py new file mode 100644 index 0000000..daf4bb3 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/dags/partner_registry_export.py @@ -0,0 +1,32 @@ +"""Nightly export of the partner registry for the finance team.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="0 1 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + tags=["partners"], +) +def partner_registry_export(): + @task + def export(ds=None) -> str: + registry = json.loads((PROJECT_ROOT / "data" / "partners.json").read_text()) + out = PROJECT_ROOT / "output" / "partner_registry" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(sorted(registry), indent=2)) + return str(out) + + export() + + +partner_registry_export() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-02/acme.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-02/acme.csv new file mode 100644 index 0000000..565ffb5 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-02/acme.csv @@ -0,0 +1,11 @@ +order_id,amount +ACM-20260902-000,298.36 +ACM-20260902-001,69.56 +ACM-20260902-002,458.39 +ACM-20260902-003,239.66 +ACM-20260902-004,292.52 +ACM-20260902-005,304.77 +ACM-20260902-006,454.87 +ACM-20260902-007,237.27 +ACM-20260902-008,277.64 +ACM-20260902-009,99.91 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/acme.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/acme.csv new file mode 100644 index 0000000..c144ded --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/acme.csv @@ -0,0 +1,26 @@ +order_id,amount +ACM-20260903-000,237.78 +ACM-20260903-001,419.05 +ACM-20260903-002,240.79 +ACM-20260903-003,321.34 +ACM-20260903-004,79.56 +ACM-20260903-005,319.26 +ACM-20260903-006,434.68 +ACM-20260903-007,263.97 +ACM-20260903-008,371.92 +ACM-20260903-009,337.35 +ACM-20260903-010,36.70 +ACM-20260903-011,380.32 +ACM-20260903-012,297.59 +ACM-20260903-013,154.13 +ACM-20260903-014,20.35 +ACM-20260903-015,433.44 +ACM-20260903-016,239.01 +ACM-20260903-017,360.82 +ACM-20260903-018,440.01 +ACM-20260903-019,358.49 +ACM-20260903-020,460.94 +ACM-20260903-021,200.51 +ACM-20260903-022,401.45 +ACM-20260903-023,225.09 +ACM-20260903-024,468.12 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/globex.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/globex.csv new file mode 100644 index 0000000..eeb7a8f --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/globex.csv @@ -0,0 +1,15 @@ +order_id,amount +GLO-20260903-000,53.24 +GLO-20260903-001,72.30 +GLO-20260903-002,112.41 +GLO-20260903-003,482.91 +GLO-20260903-004,220.90 +GLO-20260903-005,315.19 +GLO-20260903-006,154.01 +GLO-20260903-007,256.09 +GLO-20260903-008,196.00 +GLO-20260903-009,178.70 +GLO-20260903-010,294.61 +GLO-20260903-011,294.20 +GLO-20260903-012,452.58 +GLO-20260903-013,342.58 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/initech.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/initech.csv new file mode 100644 index 0000000..8362af0 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/initech.csv @@ -0,0 +1,4 @@ +order_id,amount +INI-20260903-000,428.92 +INI-20260903-001,495.54 +INI-20260903-002,337.28 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/umbrella.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/umbrella.csv new file mode 100644 index 0000000..bf94031 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-03/umbrella.csv @@ -0,0 +1,9 @@ +order_id,amount +UMB-20260903-000,350.81 +UMB-20260903-001,166.56 +UMB-20260903-002,273.17 +UMB-20260903-003,288.12 +UMB-20260903-004,56.52 +UMB-20260903-005,329.48 +UMB-20260903-006,318.32 +UMB-20260903-007,494.10 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/globex.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/globex.csv new file mode 100644 index 0000000..0a546c2 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/globex.csv @@ -0,0 +1,12 @@ +order_id,amount +GLO-20260904-000,146.05 +GLO-20260904-001,36.41 +GLO-20260904-002,427.70 +GLO-20260904-003,494.95 +GLO-20260904-004,48.82 +GLO-20260904-005,401.29 +GLO-20260904-006,208.18 +GLO-20260904-007,79.63 +GLO-20260904-008,150.48 +GLO-20260904-009,385.55 +GLO-20260904-010,437.02 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/hooli.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/hooli.csv new file mode 100644 index 0000000..a3aa09d --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partner_drop/2026-09-04/hooli.csv @@ -0,0 +1,5 @@ +order_id,amount +HOO-20260904-000,304.47 +HOO-20260904-001,382.01 +HOO-20260904-002,192.01 +HOO-20260904-003,295.26 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partners.json b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partners.json new file mode 100644 index 0000000..a6a2431 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/data/partners.json @@ -0,0 +1,7 @@ +{ + "acme": {"name": "Acme Retail", "region": "NA"}, + "globex": {"name": "Globex Marketplace", "region": "EU"}, + "hooli": {"name": "Hooli Shop", "region": "NA"}, + "initech": {"name": "Initech Supplies", "region": "NA"}, + "umbrella": {"name": "Umbrella Pharmacy", "region": "APAC"} +} diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/requirements.txt b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/requirements.txt new file mode 100644 index 0000000..af52d9b --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/fixture/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +pandas==3.0.5 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grade.py b/evals/airflow/cases/authoring-partner-feeds-mapping/grade.py new file mode 100644 index 0000000..b76a713 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grade.py @@ -0,0 +1,174 @@ +"""Grader for authoring-partner-feeds-mapping (Airflow 3.3). + +Primary checks: the DAG imports; its task set does not depend on which files +exist at parse time and it uses dynamic task mapping; `airflow dags test` for +days with 0, 1 and 4 partner files succeeds, creates exactly one mapped task +instance per file, writes correct per-partner and summary files (the summary +also on the empty day) and a rerun reproduces the same outputs. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "partner_feeds" +FIXTURE_DATA = CASE_DIR / "fixture" / "data" +# Grader-owned drops differ from the sample files and include a partner (wayne) that is not in +# data/partners.json: the set of partners must come from the files, not the registry. +GRADER_DROPS = CASE_DIR / "grader_data" / "partner_drop" +SCENARIOS = {"2026-09-01": 0, "2026-09-02": 1, "2026-09-03": 4} +TOL = 0.05 +DAGS_TEST_TIMEOUT_S = 240 + + +def expected(day: str) -> tuple[dict[str, dict], dict]: + per: dict[str, dict] = {} + folder = GRADER_DROPS / day + for p in sorted(folder.glob("*.csv")) if folder.is_dir() else []: + with p.open(newline="") as fh: + amounts = [float(r["amount"]) for r in csv.DictReader(fh)] + per[p.stem] = {"partner": p.stem, "rows": len(amounts), "total_amount": round(sum(amounts), 2)} + summary = {"files": len(per), "rows": sum(v["rows"] for v in per.values()), + "total_amount": round(sum(v["total_amount"] for v in per.values()), 2)} + return per, summary + + +def close(got: dict | None, want: dict) -> bool: + if not isinstance(got, dict): + return False + for k, v in want.items(): + if k not in got: + return False + if isinstance(v, float): + try: + if abs(float(got[k]) - v) > TOL: + return False + except (TypeError, ValueError): + return False + elif got[k] != v: + return False + return True + + +def load_json(path: Path): + try: + return json.loads(path.read_text()) + except (OSError, ValueError): + return None + + +def check_outputs(ws: Path, day: str) -> tuple[bool, bool, str]: + """(partner files ok, summary ok, detail).""" + per_want, sum_want = expected(day) + out = ws / "output" / "partner_feeds" / day + per_ok = all(close(load_json(out / f"{p}.json"), w) for p, w in per_want.items()) + got_sum = load_json(out / "summary.json") + sum_ok = close(got_sum, sum_want) + detail = f"{day}: summary expected {sum_want} got {got_sum}" + if not per_ok: + detail += f"; partner files expected {per_want} got " + str( + {p: load_json(out / f"{p}.json") for p in per_want}) + return per_ok, sum_ok, detail + + +def mapped_counts(runs: list[dict], day: str) -> dict[str, int]: + """task_id -> number of expanded (map_index >= 0) task instances in the run for ``day``.""" + counts: dict[str, int] = {} + for r in runs: + if not (r.get("logical_date") or "").startswith(day): + continue + for key in r["tasks"]: + if "[" in key: + tid = key.split("[", 1)[0] + counts[tid] = counts.get(tid, 0) + 1 + return counts + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + shutil.rmtree(ws / "output", ignore_errors=True) + shutil.rmtree(ws / "data", ignore_errors=True) + shutil.copytree(FIXTURE_DATA, ws / "data") + shutil.rmtree(ws / "data" / "partner_drop") + shutil.copytree(GRADER_DROPS, ws / "data" / "partner_drop") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is None: + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + return + + # The task set must not depend on the files present when the file is parsed. + no_drops = g.copy_workspace(ws) + shutil.rmtree(no_drops / "data" / "partner_drop", ignore_errors=True) + more_drops = g.copy_workspace(ws) + extra = more_drops / "data" / "partner_drop" / "2026-09-05" + extra.mkdir(parents=True) + for name in ("zeta", "omega", "sigma"): + (extra / f"{name}.csv").write_text("order_id,amount\nX-1,10.00\n") + shapes = {} + for label, root in (("fixture data", ws), ("no partner_drop dir", no_drops), ("extra files", more_drops)): + res = imp if root is ws else g.import_dags(py, root) + d = res["dags"].get(DAG_ID) + shapes[label] = d["tasks"] if d else f"not loaded: {res['import_errors'] or res['probe_error']}" + stable = all(v == shapes["fixture data"] for v in shapes.values()) + grader.primary("task set is independent of files present at parse time", stable, json.dumps(shapes)) + mapped = [t for t, d in dag["task_details"].items() if d["mapped"]] + grader.primary("uses dynamic task mapping", bool(mapped), f"mapped tasks: {mapped}") + + env = g.airflow_env(ws) + results = {} + for day in SCENARIOS: + # A failed task waits out its retry_delay under `dags test`; stop at the first failure. + results[day] = g.run_dags_test(py, ws, DAG_ID, day, env=env, timeout=DAGS_TEST_TIMEOUT_S) + if not results[day].ok: + break + all_ok = len(results) == len(SCENARIOS) and all(r.ok for r in results.values()) + bad = {d: r for d, r in results.items() if not r.ok} + grader.primary("airflow dags test succeeds for 0, 1 and N files", all_ok, + "" if all_ok else "; ".join(f"{d}: failed tasks {r.failed_tasks()}\n{r.log[-1500:]}" + for d, r in bad.items())) + + runs = results[max(results)].runs + per_day = {day: mapped_counts(runs, day) for day in SCENARIOS} + one_per_file = any( + all(per_day[day].get(t, 0) == n for day, n in SCENARIOS.items()) for t in mapped) + grader.primary("one mapped task instance per file (0/1/N)", all_ok and one_per_file, + f"expected {SCENARIOS}; expanded instances per mapped task: {per_day}") + + outs = {day: check_outputs(ws, day) for day in SCENARIOS} + grader.primary("per-partner result files are correct", + all(o[0] for o in outs.values()), " | ".join(o[2] for o in outs.values() if not o[0])) + grader.primary("summary is correct, including the day with no files", + all(o[1] for o in outs.values()), " | ".join(o[2] for o in outs.values() if not o[1])) + + rerun_day = "2026-09-03" + again = g.run_dags_test(py, ws, DAG_ID, rerun_day, env=env, timeout=DAGS_TEST_TIMEOUT_S) if all_ok else None + re_per, re_sum, re_det = check_outputs(ws, rerun_day) + grader.primary("rerun of a day reproduces the same outputs", bool(again and again.ok) and re_per and re_sum, + re_det if again else "skipped: first runs failed") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-02/globex.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-02/globex.csv new file mode 100644 index 0000000..d37f389 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-02/globex.csv @@ -0,0 +1,7 @@ +order_id,amount +GLO-20260902-000,205.72 +GLO-20260902-001,366.21 +GLO-20260902-002,339.04 +GLO-20260902-003,321.72 +GLO-20260902-004,357.86 +GLO-20260902-005,361.01 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/acme.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/acme.csv new file mode 100644 index 0000000..e235fa9 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/acme.csv @@ -0,0 +1,7 @@ +order_id,amount +ACM-20260903-000,411.83 +ACM-20260903-001,29.67 +ACM-20260903-002,355.14 +ACM-20260903-003,499.90 +ACM-20260903-004,236.13 +ACM-20260903-005,434.65 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/hooli.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/hooli.csv new file mode 100644 index 0000000..d6fc657 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/hooli.csv @@ -0,0 +1,8 @@ +order_id,amount +HOO-20260903-000,217.75 +HOO-20260903-001,407.27 +HOO-20260903-002,251.18 +HOO-20260903-003,46.84 +HOO-20260903-004,308.59 +HOO-20260903-005,177.85 +HOO-20260903-006,462.42 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/initech.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/initech.csv new file mode 100644 index 0000000..d6446d3 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/initech.csv @@ -0,0 +1,9 @@ +order_id,amount +INI-20260903-000,164.49 +INI-20260903-001,422.13 +INI-20260903-002,392.42 +INI-20260903-003,467.95 +INI-20260903-004,449.28 +INI-20260903-005,125.32 +INI-20260903-006,8.39 +INI-20260903-007,142.89 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/wayne.csv b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/wayne.csv new file mode 100644 index 0000000..84e1c3f --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/grader_data/partner_drop/2026-09-03/wayne.csv @@ -0,0 +1,10 @@ +order_id,amount +WAY-20260903-000,258.41 +WAY-20260903-001,274.72 +WAY-20260903-002,105.30 +WAY-20260903-003,387.58 +WAY-20260903-004,268.40 +WAY-20260903-005,86.42 +WAY-20260903-006,138.12 +WAY-20260903-007,177.97 +WAY-20260903-008,279.85 diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/missing_folder_crashes/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/missing_folder_crashes/dags/partner_feeds.py new file mode 100644 index 0000000..4f93cba --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/missing_folder_crashes/dags/partner_feeds.py @@ -0,0 +1,65 @@ +"""Process each partner file dropped for the run date as its own mapped task instance.""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task +from airflow.sdk import TriggerRule + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DROP_DIR = PROJECT_ROOT / "data" / "partner_drop" +OUTPUT_DIR = PROJECT_ROOT / "output" / "partner_feeds" + + +@dag( + schedule="0 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["partners"], +) +def partner_feeds(): + @task + def list_partner_files(ds=None) -> list[str]: + day_dir = DROP_DIR / ds + return sorted(str(p) for p in day_dir.iterdir() if p.suffix == ".csv") + + @task(map_index_template="{{ partner }}") + def process_file(path: str, ds=None) -> dict: + from airflow.sdk import get_current_context + + partner = Path(path).stem + get_current_context()["partner"] = partner + rows, total = 0, 0.0 + with open(path, newline="") as fh: + for row in csv.DictReader(fh): + rows += 1 + total += float(row["amount"]) + result = {"partner": partner, "rows": rows, "total_amount": round(total, 2)} + out = OUTPUT_DIR / ds / f"{partner}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result)) + return result + + @task(trigger_rule=TriggerRule.NONE_FAILED) + def write_summary(results, ds=None) -> dict: + results = list(results or []) + summary = { + "files": len(results), + "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2), + } + out = OUTPUT_DIR / ds / "summary.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary)) + return summary + + write_summary(process_file.expand(path=list_partner_files())) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/parse_time_file_loop/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/parse_time_file_loop/dags/partner_feeds.py new file mode 100644 index 0000000..4d51f3a --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/parse_time_file_loop/dags/partner_feeds.py @@ -0,0 +1,69 @@ +"""Process each partner file dropped for the run date as its own mapped task instance.""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task +from airflow.sdk import TriggerRule + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DROP_DIR = PROJECT_ROOT / "data" / "partner_drop" +OUTPUT_DIR = PROJECT_ROOT / "output" / "partner_feeds" + + +@dag( + schedule="0 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["partners"], +) +def partner_feeds(): + @task + def list_partner_files(ds=None) -> list[str]: + day_dir = DROP_DIR / ds + return sorted(str(p) for p in day_dir.glob("*.csv")) if day_dir.is_dir() else [] + + @task(map_index_template="{{ partner }}") + def process_file(path: str, ds=None) -> dict: + from airflow.sdk import get_current_context + + partner = Path(path).stem + get_current_context()["partner"] = partner + rows, total = 0, 0.0 + with open(path, newline="") as fh: + for row in csv.DictReader(fh): + rows += 1 + total += float(row["amount"]) + result = {"partner": partner, "rows": rows, "total_amount": round(total, 2)} + out = OUTPUT_DIR / ds / f"{partner}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result)) + return result + + @task(trigger_rule=TriggerRule.NONE_FAILED) + def write_summary(results, ds=None) -> dict: + results = list(results or []) + summary = { + "files": len(results), + "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2), + } + out = OUTPUT_DIR / ds / "summary.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary)) + return summary + + results = [] + for day_dir in sorted(DROP_DIR.glob("*")): + for f in sorted(day_dir.glob("*.csv")): + results.append(process_file.override(task_id=f"process_{day_dir.name}_{f.stem}".replace("-", "_"))(str(f))) + list_partner_files() >> write_summary(results) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/registry_driven_mapping/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/registry_driven_mapping/dags/partner_feeds.py new file mode 100644 index 0000000..760f841 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/registry_driven_mapping/dags/partner_feeds.py @@ -0,0 +1,44 @@ +"""Partner feeds mapped over the partner registry (only registered partners are picked up).""" + +from __future__ import annotations + +import csv +import json +from pathlib import Path + +import pendulum +from airflow.sdk import TriggerRule, dag, task + +ROOT = Path(__file__).resolve().parents[1] +DROP = ROOT / "data" / "partner_drop" +OUT = ROOT / "output" / "partner_feeds" + + +@dag(schedule="0 2 * * *", start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), catchup=False) +def partner_feeds(): + @task + def files_for_day(ds=None) -> list[str]: + registry = json.loads((ROOT / "data" / "partners.json").read_text()) + return [str(DROP / ds / f"{p}.csv") for p in sorted(registry) if (DROP / ds / f"{p}.csv").exists()] + + @task + def process(path: str, ds=None) -> dict: + with open(path, newline="") as fh: + amounts = [float(r["amount"]) for r in csv.DictReader(fh)] + res = {"partner": Path(path).stem, "rows": len(amounts), "total_amount": round(sum(amounts), 2)} + (OUT / ds).mkdir(parents=True, exist_ok=True) + (OUT / ds / f"{res['partner']}.json").write_text(json.dumps(res)) + return res + + @task(trigger_rule=TriggerRule.NONE_FAILED) + def summary(results, ds=None) -> None: + results = list(results or []) + (OUT / ds).mkdir(parents=True, exist_ok=True) + (OUT / ds / "summary.json").write_text(json.dumps({ + "files": len(results), "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2)})) + + summary(process.expand(path=files_for_day())) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/single_task_loops_files/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/single_task_loops_files/dags/partner_feeds.py new file mode 100644 index 0000000..67788a9 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/single_task_loops_files/dags/partner_feeds.py @@ -0,0 +1,66 @@ +"""Process each partner file dropped for the run date as its own mapped task instance.""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task +from airflow.sdk import TriggerRule + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DROP_DIR = PROJECT_ROOT / "data" / "partner_drop" +OUTPUT_DIR = PROJECT_ROOT / "output" / "partner_feeds" + + +@dag( + schedule="0 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["partners"], +) +def partner_feeds(): + @task + def list_partner_files(ds=None) -> list[str]: + day_dir = DROP_DIR / ds + return sorted(str(p) for p in day_dir.glob("*.csv")) if day_dir.is_dir() else [] + + @task(map_index_template="{{ partner }}") + def process_file(path: str, ds=None) -> dict: + partner = Path(path).stem + rows, total = 0, 0.0 + with open(path, newline="") as fh: + for row in csv.DictReader(fh): + rows += 1 + total += float(row["amount"]) + result = {"partner": partner, "rows": rows, "total_amount": round(total, 2)} + out = OUTPUT_DIR / ds / f"{partner}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result)) + return result + + @task(trigger_rule=TriggerRule.NONE_FAILED) + def write_summary(results, ds=None) -> dict: + results = list(results or []) + summary = { + "files": len(results), + "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2), + } + out = OUTPUT_DIR / ds / "summary.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary)) + return summary + + @task + def process_all(paths: list[str], ds=None) -> list[dict]: + return [process_file.function(p, ds=ds) for p in paths] + + write_summary(process_all(list_partner_files())) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/summary_default_trigger_rule/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/summary_default_trigger_rule/dags/partner_feeds.py new file mode 100644 index 0000000..0178cf5 --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/mutants/summary_default_trigger_rule/dags/partner_feeds.py @@ -0,0 +1,65 @@ +"""Process each partner file dropped for the run date as its own mapped task instance.""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task +from airflow.sdk import TriggerRule + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DROP_DIR = PROJECT_ROOT / "data" / "partner_drop" +OUTPUT_DIR = PROJECT_ROOT / "output" / "partner_feeds" + + +@dag( + schedule="0 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["partners"], +) +def partner_feeds(): + @task + def list_partner_files(ds=None) -> list[str]: + day_dir = DROP_DIR / ds + return sorted(str(p) for p in day_dir.glob("*.csv")) if day_dir.is_dir() else [] + + @task(map_index_template="{{ partner }}") + def process_file(path: str, ds=None) -> dict: + from airflow.sdk import get_current_context + + partner = Path(path).stem + get_current_context()["partner"] = partner + rows, total = 0, 0.0 + with open(path, newline="") as fh: + for row in csv.DictReader(fh): + rows += 1 + total += float(row["amount"]) + result = {"partner": partner, "rows": rows, "total_amount": round(total, 2)} + out = OUTPUT_DIR / ds / f"{partner}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result)) + return result + + @task + def write_summary(results, ds=None) -> dict: + results = list(results or []) + summary = { + "files": len(results), + "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2), + } + out = OUTPUT_DIR / ds / "summary.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary)) + return summary + + write_summary(process_file.expand(path=list_partner_files())) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-partner-feeds-mapping/reference/dags/partner_feeds.py b/evals/airflow/cases/authoring-partner-feeds-mapping/reference/dags/partner_feeds.py new file mode 100644 index 0000000..2825e8c --- /dev/null +++ b/evals/airflow/cases/authoring-partner-feeds-mapping/reference/dags/partner_feeds.py @@ -0,0 +1,65 @@ +"""Process each partner file dropped for the run date as its own mapped task instance.""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task +from airflow.sdk import TriggerRule + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DROP_DIR = PROJECT_ROOT / "data" / "partner_drop" +OUTPUT_DIR = PROJECT_ROOT / "output" / "partner_feeds" + + +@dag( + schedule="0 2 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["partners"], +) +def partner_feeds(): + @task + def list_partner_files(ds=None) -> list[str]: + day_dir = DROP_DIR / ds + return sorted(str(p) for p in day_dir.glob("*.csv")) if day_dir.is_dir() else [] + + @task(map_index_template="{{ partner }}") + def process_file(path: str, ds=None) -> dict: + from airflow.sdk import get_current_context + + partner = Path(path).stem + get_current_context()["partner"] = partner + rows, total = 0, 0.0 + with open(path, newline="") as fh: + for row in csv.DictReader(fh): + rows += 1 + total += float(row["amount"]) + result = {"partner": partner, "rows": rows, "total_amount": round(total, 2)} + out = OUTPUT_DIR / ds / f"{partner}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result)) + return result + + @task(trigger_rule=TriggerRule.NONE_FAILED) + def write_summary(results, ds=None) -> dict: + results = list(results or []) + summary = { + "files": len(results), + "rows": sum(r["rows"] for r in results), + "total_amount": round(sum(r["total_amount"] for r in results), 2), + } + out = OUTPUT_DIR / ds / "summary.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary)) + return summary + + write_summary(process_file.expand(path=list_partner_files())) + + +partner_feeds() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/alt_reference/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/alt_reference/dags/vendor_shipments.py new file mode 100644 index 0000000..78e04ef --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/alt_reference/dags/vendor_shipments.py @@ -0,0 +1,57 @@ +"""Vendor shipments DAG written with TaskFlow sensors and a TaskFlow bash task.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.sdk import PokeReturnValue, Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" + + +def vendor_file(ds: str) -> Path: + return Path(Variable.get("vendor_landing_dir")) / f"shipments_{ds}.csv" + + +@dag( + dag_id="vendor_shipments", + schedule="0 6 * * *", + start_date=datetime(2026, 9, 1, tzinfo=timezone.utc), + catchup=False, + max_active_runs=1, + tags=["shipments"], +) +def build(): + @task.sensor(mode="reschedule", poke_interval=600, timeout=timedelta(hours=17)) + def vendor_file_arrived(ds=None) -> PokeReturnValue: + path = vendor_file(ds) + return PokeReturnValue(is_done=path.exists(), xcom_value=str(path)) + + @task + def load(path: str, ds=None) -> int: + import duckdb + import pandas as pd + + frame = pd.read_csv(path, dtype={"shipment_id": str}) + frame["ship_date"] = pd.to_datetime(ds).date() + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.register("incoming", frame) + con.execute("CREATE TABLE IF NOT EXISTS vendor_shipments AS SELECT * FROM incoming LIMIT 0") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = ?", [frame["ship_date"].iloc[0]]) + con.execute("INSERT INTO vendor_shipments SELECT * FROM incoming") + finally: + con.close() + return len(frame) + + @task.bash + def publish_report(rows: int, ds=None) -> str: + return f"bash {PROJECT_ROOT / 'scripts' / 'publish_shipments_report.sh'} {ds}" + + publish_report(load(vendor_file_arrived())) + + +build() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/case.yaml b/evals/airflow/cases/authoring-vendor-shipments-etl/case.yaml new file mode 100644 index 0000000..96b463b --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/case.yaml @@ -0,0 +1,48 @@ +id: authoring-vendor-shipments-etl +area: authoring +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1500 +prompt: | + Our carrier aggregator drops a daily file called shipments_YYYY-MM-DD.csv + into a landing directory. The directory is different in every environment, + so its path lives in the Airflow Variable `vendor_landing_dir`. + + Please add a new DAG `vendor_shipments` that runs every morning at 06:00 UTC + and does this: + + 1. Waits for that day's file. It usually shows up before 9am but is + sometimes late. If it still isn't there by the end of the day, the run + should fail so on-call gets paged. We only have a couple of worker slots, + so waiting for the file must not tie one of them up. + 2. Loads the file into a `vendor_shipments` table in our DuckDB warehouse: + the file's columns plus a `ship_date` column set to the file's date. If a + day gets rerun, its rows must not be duplicated. + 3. Runs scripts/publish_shipments_report.sh for that date to publish the + carrier report. + + The aggregator has been sending files since 2026-09-01, and we need to be + able to rerun any day since then by hand, but the DAG must not backfill old + days on its own when we deploy it. Follow the conventions of the existing + DAG. +description: | + Version-aware authoring of a new Airflow 3.3 ETL DAG from a spec: sensor + + Python load + BashOperator. Pain points: outdated/hallucinated APIs (P1), + top-level Variable access (P3), poke sensors with the 7-day default timeout + (P8), non-idempotent loads (P4), BashOperator relative paths. + Primary: DAG imports with no Variables defined; parsing with Variable / + Connection lookups trapped and a poisoned metadata-DB URL records no access; + sensor -> load -> bash ordering; every sensor is reschedule-mode or + deferrable with a timeout <= 24h; daily 06:00 UTC schedule, catchup off; `airflow + dags test` for two days (Variable points at a grader-owned landing dir whose + files differ from the sample data) loads exactly the file rows, the report + script output is correct, and a rerun keeps row counts; on a day whose file + has not landed, nothing downstream of the sensor starts within 150 s (catches + sensors on the directory or a wrong path). The load step may be Python or + bash. The grader defines an + `fs_default` connection (conn_type fs) so FileSensor solutions work. + Mutants: poke sensor without timeout, module-level Variable.get with a + default, Airflow-2 `schedule_interval`, bash script by relative path, + append-only load, sensor watching the landing directory instead of the file. diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/.gitignore b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/.gitignore new file mode 100644 index 0000000..92ddf23 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/.gitignore @@ -0,0 +1,4 @@ +warehouse/ +reports/ +output/ +__pycache__/ diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/README.md b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/README.md new file mode 100644 index 0000000..fd63fa1 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/README.md @@ -0,0 +1,16 @@ +# logistics-pipelines + +Airflow DAGs for the logistics analytics team. + +- `dags/` holds the DAGs. The project runs on Airflow 3.3 (see `requirements.txt`). +- The local warehouse is a DuckDB file at `warehouse/analytics.duckdb`. DAGs create + their tables on first use. +- `scripts/` has helper scripts that DAGs call. +- `data/` has sample inputs for local development. + +Local development: + +```bash +pip install -r requirements.txt +airflow dags test +``` diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/dags/warehouse_orders.py b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/dags/warehouse_orders.py new file mode 100644 index 0000000..c6d9c2b --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/dags/warehouse_orders.py @@ -0,0 +1,53 @@ +"""Load the daily order export into the warehouse and refresh the orders mart.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +ORDERS_EXPORT = PROJECT_ROOT / "data" / "orders_export.csv" + + +@dag( + schedule="0 4 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["orders", "warehouse"], +) +def warehouse_orders(): + @task + def load_orders(ds=None): + import duckdb + + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS orders (order_id VARCHAR, amount DOUBLE, order_date DATE)" + ) + # Replace the day's rows so a rerun never duplicates them. + con.execute("DELETE FROM orders WHERE order_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO orders SELECT order_id, amount, CAST(order_date AS DATE) " + "FROM read_csv_auto(?) WHERE CAST(order_date AS DATE) = CAST(? AS DATE)", + [str(ORDERS_EXPORT), ds], + ) + return con.execute( + "SELECT count(*) FROM orders WHERE order_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + refresh_orders_mart = BashOperator( + task_id="refresh_orders_mart", + bash_command=f"{PROJECT_ROOT}/scripts/refresh_orders_mart.sh {{{{ ds }}}}", + ) + + load_orders() >> refresh_orders_mart + + +warehouse_orders() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/incoming/shipments_2026-09-28.csv b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/incoming/shipments_2026-09-28.csv new file mode 100644 index 0000000..9b18b10 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/incoming/shipments_2026-09-28.csv @@ -0,0 +1,26 @@ +shipment_id,carrier,destination,weight_kg,shipped_at +SHP-20260928-0000,DHL,Chicago,4.89,2026-09-28 04:36:00 +SHP-20260928-0001,USPS,Boston,3.94,2026-09-28 14:30:00 +SHP-20260928-0002,USPS,Denver,24.38,2026-09-28 00:57:00 +SHP-20260928-0003,USPS,Chicago,28.92,2026-09-28 00:44:00 +SHP-20260928-0004,DHL,Chicago,1.42,2026-09-28 07:37:00 +SHP-20260928-0005,DHL,Denver,27.52,2026-09-28 00:41:00 +SHP-20260928-0006,DHL,Seattle,9.02,2026-09-28 13:46:00 +SHP-20260928-0007,FedEx,Chicago,9.39,2026-09-28 14:31:00 +SHP-20260928-0008,USPS,Chicago,37.07,2026-09-28 07:48:00 +SHP-20260928-0009,DHL,Boston,25.25,2026-09-28 13:53:00 +SHP-20260928-0010,UPS,Austin,29.78,2026-09-28 23:55:00 +SHP-20260928-0011,USPS,Seattle,33.24,2026-09-28 23:45:00 +SHP-20260928-0012,UPS,Chicago,23.59,2026-09-28 21:12:00 +SHP-20260928-0013,USPS,Seattle,34.16,2026-09-28 15:54:00 +SHP-20260928-0014,USPS,Denver,26.66,2026-09-28 15:15:00 +SHP-20260928-0015,UPS,Austin,17.67,2026-09-28 11:35:00 +SHP-20260928-0016,FedEx,Seattle,33.63,2026-09-28 16:06:00 +SHP-20260928-0017,DHL,Denver,1.93,2026-09-28 11:31:00 +SHP-20260928-0018,USPS,Boston,6.91,2026-09-28 22:54:00 +SHP-20260928-0019,FedEx,Seattle,36.82,2026-09-28 07:00:00 +SHP-20260928-0020,USPS,Seattle,13.88,2026-09-28 17:14:00 +SHP-20260928-0021,USPS,Chicago,26.44,2026-09-28 18:22:00 +SHP-20260928-0022,DHL,Denver,31.39,2026-09-28 19:46:00 +SHP-20260928-0023,FedEx,Seattle,31.14,2026-09-28 23:32:00 +SHP-20260928-0024,DHL,Denver,34.83,2026-09-28 06:27:00 diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/orders_export.csv b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/orders_export.csv new file mode 100644 index 0000000..d59defa --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/data/orders_export.csv @@ -0,0 +1,7 @@ +order_id,amount,order_date +O-1001,120.50,2026-09-27 +O-1002,35.00,2026-09-27 +O-1003,89.99,2026-09-28 +O-1004,12.25,2026-09-28 +O-1005,230.00,2026-09-28 +O-1006,54.10,2026-09-29 diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/requirements.txt b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/requirements.txt new file mode 100644 index 0000000..8a1ca78 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +duckdb==1.5.6 +pandas==3.0.5 diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/build_shipments_report.py b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/build_shipments_report.py new file mode 100644 index 0000000..5a63bbc --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/build_shipments_report.py @@ -0,0 +1,36 @@ +"""Per-carrier shipment report for one ship_date, read from the vendor_shipments table.""" + +import argparse +import csv +from pathlib import Path + +import duckdb + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--warehouse", required=True) + ap.add_argument("--day", required=True) + ap.add_argument("--out", required=True) + args = ap.parse_args() + with duckdb.connect(args.warehouse, read_only=True) as con: + rows = con.execute( + "SELECT carrier, count(*) AS shipments, " + "round(sum(CAST(weight_kg AS DOUBLE)), 2) AS total_weight_kg " + "FROM vendor_shipments WHERE CAST(ship_date AS DATE) = CAST(? AS DATE) " + "GROUP BY carrier ORDER BY carrier", + [args.day], + ).fetchall() + if not rows: + raise SystemExit(f"no vendor_shipments rows for {args.day}") + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["carrier", "shipments", "total_weight_kg"]) + w.writerows(rows) + print(f"wrote {out} ({len(rows)} carriers)") + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/publish_shipments_report.sh b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/publish_shipments_report.sh new file mode 100755 index 0000000..03c92ac --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/publish_shipments_report.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Publish the per-carrier shipments report for one day. +# Usage: publish_shipments_report.sh YYYY-MM-DD +# Reads the vendor_shipments table (ship_date, carrier, weight_kg, ...) from +# warehouse/analytics.duckdb and writes reports/shipments_.csv. +set -euo pipefail +DAY="$1" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +python "$ROOT/scripts/build_shipments_report.py" --warehouse "$ROOT/warehouse/analytics.duckdb" \ + --day "$DAY" --out "$ROOT/reports/shipments_${DAY}.csv" diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/refresh_orders_mart.sh b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/refresh_orders_mart.sh new file mode 100755 index 0000000..78358f4 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/fixture/scripts/refresh_orders_mart.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Rebuild the orders mart for one day. Usage: refresh_orders_mart.sh YYYY-MM-DD +set -euo pipefail +DAY="$1" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +python - "$ROOT/warehouse/analytics.duckdb" "$DAY" <<'PY' +import sys, duckdb +db, day = sys.argv[1], sys.argv[2] +with duckdb.connect(db) as con: + con.execute("CREATE TABLE IF NOT EXISTS orders_mart (order_date DATE, orders BIGINT, revenue DOUBLE)") + con.execute("DELETE FROM orders_mart WHERE order_date = CAST(? AS DATE)", [day]) + con.execute("INSERT INTO orders_mart SELECT order_date, count(*), sum(amount) FROM orders " + "WHERE order_date = CAST(? AS DATE) GROUP BY order_date", [day]) +PY +echo "orders mart refreshed for $DAY" diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/grade.py b/evals/airflow/cases/authoring-vendor-shipments-etl/grade.py new file mode 100644 index 0000000..b7f8564 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/grade.py @@ -0,0 +1,284 @@ +"""Grader for authoring-vendor-shipments-etl (Airflow 3.3). + +Primary checks: the DAG imports (with no Variables defined), touches no +Variable/Connection/metadata DB while parsing, has sensor -> load -> bash +ordering, the sensor frees its worker slot (reschedule or deferrable) and has +an explicit timeout of at most a day, the DAG runs daily at 06:00 UTC without catchup, +`airflow dags test` loads the file named by the `vendor_landing_dir` Variable +into DuckDB, publishes the report and is idempotent on a rerun, and on a day +whose file is missing nothing downstream of the sensor starts. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +import tempfile +from collections import Counter, defaultdict +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "vendor_shipments" +DAY, NEIGHBOUR = "2026-09-29", "2026-09-28" +LANDING = CASE_DIR / "grader_data" / "landing" +MAX_SENSOR_TIMEOUT_S = 24 * 3600 +DAGS_TEST_TIMEOUT_S = 240 # a sensor looking in the wrong place waits until this kills it +MISSING_DAY = "2026-09-27" # no landing file for this day +GATE_WINDOW_S = 150 +NOT_STARTED = {"None", "none", "scheduled", "upstream_failed", "removed"} +BASH_TYPES = ("BashOperator", "_BashDecoratedOperator") + +# Parse the DAG folder with Variable/Connection lookups booby-trapped and the +# metadata DB pointed at a path that cannot exist. +PARSE_GUARD_PROBE = r''' +CALLS = [] +def _trap(label): + def fn(*a, **k): + CALLS.append(label) + raise RuntimeError(f"parse-time {label} is not allowed") + return fn +def _patch(modname, clsname, attr, label): + try: + mod = __import__(modname, fromlist=[clsname]) + cls = getattr(mod, clsname) + except Exception: + return + trap = _trap(label) + setattr(cls, attr, classmethod(lambda cls, *a, **k: trap(*a, **k))) +for _m, _c, _a, _l in [ + ("airflow.sdk.definitions.variable", "Variable", "get", "Variable.get"), + ("airflow.models.variable", "Variable", "get", "Variable.get"), + ("airflow.sdk.definitions.connection", "Connection", "get", "Connection.get"), + ("airflow.models.connection", "Connection", "get_connection_from_secrets", "Connection lookup"), + ("airflow.sdk.bases.hook", "BaseHook", "get_connection", "BaseHook.get_connection"), + ("airflow.hooks.base", "BaseHook", "get_connection", "BaseHook.get_connection"), +]: + _patch(_m, _c, _a, _l) +bag = dagbag() +RESULT = {"calls": CALLS, "dags": sorted(bag.dags), + "errors": {k: v.strip().splitlines()[-1] for k, v in bag.import_errors.items()}} +''' + +SENSOR_PROBE = r''' +from datetime import timedelta +from airflow.sdk import BaseSensorOperator +dag = get_dag("vendor_shipments") +sensors = [] +for t in dag.tasks: + if isinstance(t, BaseSensorOperator): + to = t.timeout + to = to.total_seconds() if isinstance(to, timedelta) else float(to) + sensors.append({"task_id": t.task_id, "class": type(t).__name__, "mode": t.mode, + "deferrable": bool(getattr(t, "deferrable", False) or getattr(t, "start_from_trigger", False)), + "timeout_s": to}) +RESULT = {"sensors": sensors, + "runs": scheduled_intervals(dag, "2026-10-01T00:00:00+00:00", n=3)} +''' + +TABLE_PROBE = r''' +import duckdb, os +path = os.path.join(os.getcwd(), "warehouse", "analytics.duckdb") +if not os.path.exists(path): + RESULT = {"error": "warehouse/analytics.duckdb was not created"} +else: + with duckdb.connect(path, read_only=True) as con: + tables = [r[0] for r in con.execute("SELECT table_name FROM information_schema.tables").fetchall()] + if "vendor_shipments" not in tables: + RESULT = {"error": f"no vendor_shipments table; tables: {tables}"} + else: + rows = con.execute("SELECT CAST(CAST(ship_date AS DATE) AS VARCHAR), CAST(shipment_id AS VARCHAR) " + "FROM vendor_shipments").fetchall() + RESULT = {"rows": rows} +''' + + +def landing_rows(day: str) -> list[dict]: + with (LANDING / f"shipments_{day}.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def expected_report(day: str) -> dict[str, tuple[int, float]]: + count: Counter[str] = Counter() + weight: dict[str, float] = defaultdict(float) + for r in landing_rows(day): + count[r["carrier"]] += 1 + weight[r["carrier"]] += float(r["weight_kg"]) + return {c: (count[c], round(weight[c], 2)) for c in count} + + +def read_report(ws: Path, day: str) -> dict[str, tuple[int, float]] | None: + path = ws / "reports" / f"shipments_{day}.csv" + if not path.exists(): + return None + with path.open(newline="") as fh: + return {r["carrier"]: (int(r["shipments"]), round(float(r["total_weight_kg"]), 2)) + for r in csv.DictReader(fh)} + + +def ancestors(deps: list[list[str]], node: str) -> set[str]: + ups: dict[str, set[str]] = defaultdict(set) + for u, d in deps: + ups[d].add(u) + seen: set[str] = set() + stack = [node] + while stack: + for u in ups[stack.pop()]: + if u not in seen: + seen.add(u) + stack.append(u) + return seen + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + # 1. Import with no Variables or Connections defined. + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly with no Variables defined", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + + # 2. No Variable / Connection / metadata-DB access while parsing. + poisoned = g.airflow_env(ws, extra={ + "AIRFLOW__DATABASE__SQL_ALCHEMY_CONN": "sqlite:////nonexistent-eval-dir/poisoned.db"}) + guard, proc = g.probe_json(py, PARSE_GUARD_PROBE, ws, env=poisoned) + parse_ok = bool(guard) and not guard["calls"] and DAG_ID in guard["dags"] and not guard["errors"] + grader.primary("no Variable/Connection/metadata-DB access at parse time", parse_ok, + json.dumps(guard) if guard else proc.tail(30)) + + if dag is None: + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + return + + # 3. Task graph: sensor -> python load -> bash report. + details, deps = dag["task_details"], dag["deps"] + probe, proc = g.probe_json(py, SENSOR_PROBE, ws) + sensors = (probe or {}).get("sensors", []) + sensor_ids = {s["task_id"] for s in sensors} + bash_ids = [t for t, d in details.items() if d["task_type"] in BASH_TYPES] + graph_ok, graph_detail = False, f"sensors={sorted(sensor_ids)} bash={bash_ids} deps={deps}" + for b in bash_ids: + ups = ancestors(deps, b) + # The load step may itself be a bash task (e.g. `python scripts/load.py`). + middle = [t for t in ups - sensor_ids if sensor_ids & ancestors(deps, t)] + if sensor_ids & ups and middle: + graph_ok = True + graph_detail = f"sensor(s) {sorted(sensor_ids & ups)} -> {middle} -> {b}" + break + grader.primary("task graph is wait-for-file -> load -> bash report", graph_ok, graph_detail) + + # 4. Sensor does not hold a worker slot and gives up within a day. + slot_ok = bool(sensors) and all(s["mode"] == "reschedule" or s["deferrable"] for s in sensors) + grader.primary("file sensor is reschedule-mode or deferrable", slot_ok, + json.dumps(sensors) if probe else proc.tail(30)) + timeout_ok = bool(sensors) and all(0 < s["timeout_s"] <= MAX_SENSOR_TIMEOUT_S for s in sensors) + grader.primary("file sensor has an explicit timeout of at most 24h", timeout_ok, json.dumps(sensors)) + + # 5. Schedule: one run per day, no catchup. + runs = (probe or {}).get("runs", []) + daily = len(runs) == 3 and all( + g_delta == 86400 for g_delta in _deltas([r["run_after"] for r in runs])) and all( + _utc_hm(r["run_after"]) == (6, 0) for r in runs) + grader.primary("runs daily at 06:00 UTC with catchup disabled", daily and dag["catchup"] is False, + f"catchup={dag['catchup']} runs={runs}") + + # 6. dags test: two days, then a rerun of the second day. + shutil.rmtree(ws / "warehouse", ignore_errors=True) + shutil.rmtree(ws / "reports", ignore_errors=True) + landing = Path(tempfile.mkdtemp(prefix="eval-landing-")) + shutil.copytree(LANDING, landing, dirs_exist_ok=True) + env = g.airflow_env(ws, extra={ + "AIRFLOW_VAR_VENDOR_LANDING_DIR": str(landing), + "AIRFLOW_CONN_FS_DEFAULT": json.dumps({"conn_type": "fs"}), + }) + first_n = g.run_dags_test(py, ws, DAG_ID, NEIGHBOUR, env=env, timeout=DAGS_TEST_TIMEOUT_S) + first = g.run_dags_test(py, ws, DAG_ID, DAY, env=env, timeout=DAGS_TEST_TIMEOUT_S) if first_n.ok else first_n + grader.primary("airflow dags test succeeds", first_n.ok and first.ok, + "" if first.ok and first_n.ok else + f"failed tasks: {first.failed_tasks()}\n" + first.log[-2500:]) + + def table_state() -> tuple[dict[str, Counter], str]: + res, p = g.probe_json(py, TABLE_PROBE, ws, env=env) + if not res or "rows" not in res: + return {}, (res or {}).get("error") or p.tail(20) + by_day: dict[str, Counter] = defaultdict(Counter) + for d, sid in res["rows"]: + by_day[d][sid] += 1 + return by_day, "" + + want = {d: Counter(r["shipment_id"] for r in landing_rows(d)) for d in (DAY, NEIGHBOUR)} + by_day, err = table_state() + loaded_ok = first.ok and all(by_day.get(d) == want[d] for d in want) + grader.primary("vendor_shipments holds exactly the landing file rows per ship_date", loaded_ok, + err or {d: f"expected {sum(want[d].values())} rows, got {sum(by_day.get(d, Counter()).values())}" + for d in want}) + report_ok = all(read_report(ws, d) == expected_report(d) for d in want) + grader.primary("carrier report published for each run date", first.ok and report_ok, + {d: {"expected": expected_report(d), "got": read_report(ws, d)} for d in want}) + + second = g.run_dags_test(py, ws, DAG_ID, DAY, env=env, timeout=DAGS_TEST_TIMEOUT_S) if first.ok else first + by_day2, err2 = table_state() + idem = second.ok and all(by_day2.get(d) == want[d] for d in want) and read_report(ws, DAY) == expected_report(DAY) + grader.primary("rerun of a day does not duplicate rows and keeps other days", idem, + err2 or f"second run ok={second.ok}; " + str( + {d: sum(by_day2.get(d, Counter()).values()) for d in want})) + + # 7. The sensor really waits for that day's file: on a day whose file has not landed, + # nothing downstream of a sensor may start before GATE_WINDOW_S (the run is then killed). + # Catches sensors on the landing directory, on the wrong file name, or on a stale path. + gate_ok, gate_detail = sensor_gates_downstream(py, ws, landing, sensor_ids, deps) + grader.primary("load waits for the day's file (nothing runs when the file is missing)", gate_ok, gate_detail) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +def sensor_gates_downstream(py: str, ws: Path, landing: Path, sensor_ids: set[str], + deps: list[list[str]]) -> tuple[bool, str]: + if not sensor_ids: + return False, "no sensor tasks" + gated = {t for t in {d for _, d in deps} | {u for u, _ in deps} + if t not in sensor_ids and sensor_ids & ancestors(deps, t)} + copy = g.copy_workspace(ws) + shutil.rmtree(copy / "warehouse", ignore_errors=True) + shutil.rmtree(copy / "reports", ignore_errors=True) + env = g.airflow_env(copy, extra={ + "AIRFLOW_VAR_VENDOR_LANDING_DIR": str(landing), + "AIRFLOW_CONN_FS_DEFAULT": json.dumps({"conn_type": "fs"}), + }) + res = g.run_dags_test(py, copy, DAG_ID, MISSING_DAY, env=env, timeout=GATE_WINDOW_S) + states = {k: v for run in res.runs for k, v in run.get("tasks", {}).items()} + started = {k: v for k, v in states.items() if k.split("[", 1)[0] in gated and v not in NOT_STARTED} + detail = (f"no shipments_{MISSING_DAY}.csv in the landing dir; after {GATE_WINDOW_S}s " + f"(timed_out={res.timed_out}) task states: {states}") + return not started, detail + + +def _utc_hm(stamp: str) -> tuple[int, int]: + from datetime import datetime, timezone + + t = datetime.fromisoformat(stamp).astimezone(timezone.utc) + return t.hour, t.minute + + +def _deltas(stamps: list[str]) -> list[float]: + from datetime import datetime + + ts = [datetime.fromisoformat(s) for s in stamps] + return [(b - a).total_seconds() for a, b in zip(ts, ts[1:])] + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-28.csv b/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-28.csv new file mode 100644 index 0000000..00d709e --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-28.csv @@ -0,0 +1,32 @@ +shipment_id,carrier,destination,weight_kg,shipped_at +SHP-20260928-0000,USPS,Denver,20.41,2026-09-28 14:55:00 +SHP-20260928-0001,FedEx,Seattle,19.14,2026-09-28 18:12:00 +SHP-20260928-0002,FedEx,Austin,17.97,2026-09-28 19:50:00 +SHP-20260928-0003,DHL,Seattle,39.29,2026-09-28 04:05:00 +SHP-20260928-0004,FedEx,Seattle,0.80,2026-09-28 14:41:00 +SHP-20260928-0005,DHL,Austin,7.77,2026-09-28 16:04:00 +SHP-20260928-0006,DHL,Denver,13.19,2026-09-28 07:38:00 +SHP-20260928-0007,FedEx,Seattle,9.50,2026-09-28 18:53:00 +SHP-20260928-0008,DHL,Austin,18.40,2026-09-28 09:31:00 +SHP-20260928-0009,DHL,Chicago,12.75,2026-09-28 08:26:00 +SHP-20260928-0010,UPS,Austin,2.99,2026-09-28 07:32:00 +SHP-20260928-0011,DHL,Chicago,15.58,2026-09-28 03:25:00 +SHP-20260928-0012,DHL,Boston,8.55,2026-09-28 00:54:00 +SHP-20260928-0013,USPS,Denver,16.91,2026-09-28 01:30:00 +SHP-20260928-0014,FedEx,Chicago,13.61,2026-09-28 18:40:00 +SHP-20260928-0015,DHL,Denver,30.37,2026-09-28 09:21:00 +SHP-20260928-0016,FedEx,Austin,0.64,2026-09-28 03:08:00 +SHP-20260928-0017,USPS,Boston,27.35,2026-09-28 14:51:00 +SHP-20260928-0018,FedEx,Boston,16.89,2026-09-28 06:28:00 +SHP-20260928-0019,USPS,Denver,39.60,2026-09-28 12:07:00 +SHP-20260928-0020,UPS,Austin,8.59,2026-09-28 00:17:00 +SHP-20260928-0021,DHL,Austin,39.57,2026-09-28 12:54:00 +SHP-20260928-0022,UPS,Austin,30.95,2026-09-28 06:28:00 +SHP-20260928-0023,UPS,Denver,3.12,2026-09-28 10:53:00 +SHP-20260928-0024,FedEx,Austin,24.13,2026-09-28 02:13:00 +SHP-20260928-0025,USPS,Boston,38.37,2026-09-28 11:39:00 +SHP-20260928-0026,FedEx,Denver,7.48,2026-09-28 15:53:00 +SHP-20260928-0027,FedEx,Seattle,10.13,2026-09-28 04:19:00 +SHP-20260928-0028,FedEx,Denver,35.31,2026-09-28 06:10:00 +SHP-20260928-0029,USPS,Austin,4.33,2026-09-28 19:05:00 +SHP-20260928-0030,UPS,Boston,29.65,2026-09-28 01:32:00 diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-29.csv b/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-29.csv new file mode 100644 index 0000000..8116900 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/grader_data/landing/shipments_2026-09-29.csv @@ -0,0 +1,43 @@ +shipment_id,carrier,destination,weight_kg,shipped_at +SHP-20260929-0000,UPS,Boston,15.39,2026-09-29 15:17:00 +SHP-20260929-0001,UPS,Denver,27.69,2026-09-29 11:30:00 +SHP-20260929-0002,DHL,Seattle,5.98,2026-09-29 19:14:00 +SHP-20260929-0003,FedEx,Chicago,36.26,2026-09-29 14:23:00 +SHP-20260929-0004,FedEx,Austin,20.64,2026-09-29 01:36:00 +SHP-20260929-0005,USPS,Austin,0.94,2026-09-29 10:43:00 +SHP-20260929-0006,FedEx,Austin,39.58,2026-09-29 01:52:00 +SHP-20260929-0007,DHL,Denver,38.86,2026-09-29 13:28:00 +SHP-20260929-0008,FedEx,Austin,22.29,2026-09-29 17:20:00 +SHP-20260929-0009,DHL,Denver,24.38,2026-09-29 16:52:00 +SHP-20260929-0010,USPS,Denver,24.45,2026-09-29 21:38:00 +SHP-20260929-0011,DHL,Boston,39.06,2026-09-29 17:01:00 +SHP-20260929-0012,UPS,Chicago,34.67,2026-09-29 21:16:00 +SHP-20260929-0013,UPS,Austin,10.24,2026-09-29 12:43:00 +SHP-20260929-0014,UPS,Seattle,23.01,2026-09-29 10:23:00 +SHP-20260929-0015,DHL,Denver,32.94,2026-09-29 16:11:00 +SHP-20260929-0016,DHL,Boston,27.09,2026-09-29 13:02:00 +SHP-20260929-0017,FedEx,Seattle,4.28,2026-09-29 01:24:00 +SHP-20260929-0018,FedEx,Austin,27.90,2026-09-29 17:14:00 +SHP-20260929-0019,UPS,Denver,13.90,2026-09-29 01:27:00 +SHP-20260929-0020,DHL,Seattle,18.48,2026-09-29 20:46:00 +SHP-20260929-0021,UPS,Denver,19.16,2026-09-29 06:21:00 +SHP-20260929-0022,FedEx,Denver,36.99,2026-09-29 15:15:00 +SHP-20260929-0023,FedEx,Boston,36.50,2026-09-29 11:11:00 +SHP-20260929-0024,UPS,Boston,9.82,2026-09-29 00:17:00 +SHP-20260929-0025,DHL,Boston,24.84,2026-09-29 00:32:00 +SHP-20260929-0026,FedEx,Austin,37.11,2026-09-29 11:34:00 +SHP-20260929-0027,UPS,Austin,9.33,2026-09-29 21:53:00 +SHP-20260929-0028,USPS,Denver,25.15,2026-09-29 16:32:00 +SHP-20260929-0029,USPS,Boston,18.06,2026-09-29 08:43:00 +SHP-20260929-0030,DHL,Chicago,13.33,2026-09-29 19:55:00 +SHP-20260929-0031,USPS,Boston,29.41,2026-09-29 16:40:00 +SHP-20260929-0032,DHL,Austin,27.83,2026-09-29 16:16:00 +SHP-20260929-0033,UPS,Denver,5.71,2026-09-29 04:35:00 +SHP-20260929-0034,USPS,Austin,22.49,2026-09-29 06:27:00 +SHP-20260929-0035,UPS,Austin,16.82,2026-09-29 19:46:00 +SHP-20260929-0036,USPS,Boston,23.29,2026-09-29 18:05:00 +SHP-20260929-0037,USPS,Seattle,6.60,2026-09-29 11:23:00 +SHP-20260929-0038,FedEx,Austin,31.95,2026-09-29 22:45:00 +SHP-20260929-0039,USPS,Seattle,8.17,2026-09-29 02:58:00 +SHP-20260929-0040,DHL,Chicago,7.72,2026-09-29 12:50:00 +SHP-20260929-0041,USPS,Seattle,24.52,2026-09-29 08:59:00 diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/airflow2_schedule_interval/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/airflow2_schedule_interval/dags/vendor_shipments.py new file mode 100644 index 0000000..27eeaad --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/airflow2_schedule_interval/dags/vendor_shipments.py @@ -0,0 +1,69 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule_interval="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/append_only_load/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/append_only_load/dags/vendor_shipments.py new file mode 100644 index 0000000..799b35c --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/append_only_load/dags/vendor_shipments.py @@ -0,0 +1,68 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/bash_relative_script_path/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/bash_relative_script_path/dags/vendor_shipments.py new file mode 100644 index 0000000..ac57023 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/bash_relative_script_path/dags/vendor_shipments.py @@ -0,0 +1,69 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command="scripts/publish_shipments_report.sh {{ ds }}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/poke_sensor_default_timeout/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/poke_sensor_default_timeout/dags/vendor_shipments.py new file mode 100644 index 0000000..7175ef4 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/poke_sensor_default_timeout/dags/vendor_shipments.py @@ -0,0 +1,67 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + poke_interval=60, + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/sensor_watches_landing_dir/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/sensor_watches_landing_dir/dags/vendor_shipments.py new file mode 100644 index 0000000..0a4fe83 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/sensor_watches_landing_dir/dags/vendor_shipments.py @@ -0,0 +1,69 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath="{{ var.value.vendor_landing_dir }}", + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/toplevel_variable_get/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/toplevel_variable_get/dags/vendor_shipments.py new file mode 100644 index 0000000..fb54575 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/mutants/toplevel_variable_get/dags/vendor_shipments.py @@ -0,0 +1,70 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +LANDING_DIR = Variable.get("vendor_landing_dir", default=str(PROJECT_ROOT / "data" / "incoming")) +VENDOR_FILE = LANDING_DIR + "/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/authoring-vendor-shipments-etl/reference/dags/vendor_shipments.py b/evals/airflow/cases/authoring-vendor-shipments-etl/reference/dags/vendor_shipments.py new file mode 100644 index 0000000..34c4448 --- /dev/null +++ b/evals/airflow/cases/authoring-vendor-shipments-etl/reference/dags/vendor_shipments.py @@ -0,0 +1,69 @@ +"""Daily vendor shipments: wait for the vendor file, load it, publish the carrier report.""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.sensors.filesystem import FileSensor +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" +# Rendered at run time, so parsing the file never reads the Variable. +VENDOR_FILE = "{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv" + + +@dag( + schedule="0 6 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["shipments", "warehouse"], +) +def vendor_shipments(): + wait_for_vendor_file = FileSensor( + task_id="wait_for_vendor_file", + filepath=VENDOR_FILE, + deferrable=True, + poke_interval=300, + timeout=int(timedelta(hours=18).total_seconds()), + retries=0, + ) + + @task(templates_dict={"path": VENDOR_FILE}) + def load_shipments(ds=None, templates_dict=None): + import duckdb + + path = templates_dict["path"] + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS vendor_shipments (shipment_id VARCHAR, carrier VARCHAR, " + "destination VARCHAR, weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE)" + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) + con.execute( + "INSERT INTO vendor_shipments SELECT shipment_id, carrier, destination, weight_kg, " + "shipped_at, CAST(? AS DATE) FROM read_csv(?, header = true, " + "columns = {'shipment_id': 'VARCHAR', 'carrier': 'VARCHAR', 'destination': 'VARCHAR', " + "'weight_kg': 'DOUBLE', 'shipped_at': 'TIMESTAMP'})", + [ds, path], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] + ).fetchone()[0] + + publish_shipments_report = BashOperator( + task_id="publish_shipments_report", + bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", + ) + + wait_for_vendor_file >> load_shipments() >> publish_shipments_report + + +vendor_shipments() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/alt_reference/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/alt_reference/dags/partner_feed.py new file mode 100644 index 0000000..5088ccf --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/alt_reference/dags/partner_feed.py @@ -0,0 +1,81 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. + +The feed day is resolved once, in `resolve_day`, and passed downstream. Manual +runs triggered without a logical date use the date they were triggered for. +""" + +from __future__ import annotations + +import csv +import json +import shutil +from datetime import datetime +from pathlib import Path + +from airflow.sdk import dag, get_current_context, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def resolve_day() -> str: + context = get_current_context() + logical_date = context.get("logical_date") + if logical_date is not None: + return logical_date.date().isoformat() + return context["dag_run"].run_after.date().isoformat() + + @task + def extract_orders(day: str) -> str: + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, day: str) -> str: + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + @task + def deliver(feed_path: str, manifest_path: str, day: str) -> None: + OUTBOX.mkdir(parents=True, exist_ok=True) + stamp = day.replace("-", "") + shutil.copyfile(feed_path, OUTBOX / f"acme_orders_{stamp}.csv") + shutil.copyfile(manifest_path, OUTBOX / f"acme_orders_{stamp}.manifest.json") + + day = resolve_day() + feed = extract_orders(day) + manifest = write_manifest(feed, day) + deliver(feed, manifest, day) + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/case.yaml b/evals/airflow/cases/debugging-manual-trigger-context/case.yaml new file mode 100644 index 0000000..8f4addb --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/case.yaml @@ -0,0 +1,47 @@ +id: debugging-manual-trigger-context +area: debugging +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + partner_feed has been failing since we upgraded to Airflow 3.3 last week, and + it fails differently depending on how it's started: + + - the nightly scheduled run dies in write_manifest with `KeyError: 'execution_date'` + - when I trigger it from the UI to get the partner their file (I leave the + logical date empty), it dies earlier, in extract_orders, with + `KeyError: 'logical_date'` + + The DAG file parses fine and there's no scheduler on my laptop, so I can't + just watch it. Can you debug and fix it? Requirements: + - a normal run (scheduled, or triggered with a logical date) produces the feed + for its logical date, same as before + - a manual trigger without a date should produce the feed for the day it was + triggered (UTC). If we clear and re-run that run tomorrow it still has to + produce the same day's feed, not tomorrow's + - keep the output/outbox file names and the extract -> manifest -> deliver + steps; ACME's sync depends on them +description: | + Debugging runtime-only failures on Airflow 3.3 without a live scheduler. The + fixture DAG parses cleanly but uses the removed `execution_date` context key + (fails every run), `context["logical_date"]` (KeyError on manual runs, which + have `logical_date=None` in Airflow 3), and a `{{ ds_nodash }}` BashOperator + template that is undefined on those runs. That third failure only appears + after the first two are fixed. Pain points: P2 logical_date/data-interval + semantics, removed context keys, and verifying both trigger paths (ruff AIR301 + misses keys reached through templates and `**context`). + Primary checks: DAGs import; the extract -> manifest -> deliver chain is kept; + `airflow dags test partner_feed 2026-03-02` succeeds and delivers exactly the + 2026-03-02 feed, manifest and outbox copies; a manual run with no logical date + (`dag.test(logical_date=None, run_after=2026-03-03T00:30Z)`, executed as a + clear-and-re-run the next day: DagRun `start_date` 2026-03-04) succeeds and + delivers exactly the 2026-03-03 files. Both runs use TZ=America/Los_Angeles, + so a day taken in local time instead of UTC is caught on any grading machine. + Mutants: `now_fallback` (wall-clock `now()` when logical_date is missing), + `skip_manual_runs` (AirflowSkipException on manual runs), `template_left` + (Python fixed, `{{ ds_nodash }}` template left in place), `run_after_always` + (uses run_after even when a logical date was given), `data_interval_fallback` + (uses `data_interval_start`, which is None on runs without a logical date), + `start_date_fallback` (DagRun `start_date`, which moves when the run is + cleared), `local_tz_day` (trigger day in the worker's local timezone). diff --git a/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_catalog_sync.py b/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_catalog_sync.py new file mode 100644 index 0000000..7fcc0f6 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_catalog_sync.py @@ -0,0 +1,33 @@ +"""Weekly list of SKUs we have sold to ACME, for their catalogue team.""" + +from __future__ import annotations + +import csv +from datetime import datetime +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="0 6 * * 1", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_catalog_sync(): + @task + def export_skus() -> str: + with (PROJECT_ROOT / "data" / "partner_orders.csv").open(newline="") as fh: + skus = sorted({r["sku"] for r in csv.DictReader(fh) if r["partner"] == "acme"}) + out = PROJECT_ROOT / "output" / "partner_catalog" / "acme_skus.txt" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text("\n".join(skus) + "\n") + return str(out) + + export_skus() + + +partner_catalog_sync() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_feed.py new file mode 100644 index 0000000..fffc488 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/fixture/dags/partner_feed.py @@ -0,0 +1,74 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = context["logical_date"].strftime("%Y-%m-%d") + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = context["execution_date"].strftime("%Y-%m-%d") + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/fixture/data/partner_orders.csv b/evals/airflow/cases/debugging-manual-trigger-context/fixture/data/partner_orders.csv new file mode 100644 index 0000000..93315ea --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/fixture/data/partner_orders.csv @@ -0,0 +1,43 @@ +order_id,order_date,partner,sku,qty,amount +5001,2026-03-01,initech,SKU-1001,2,25.39 +5002,2026-03-01,acme,SKU-2040,5,193.61 +5003,2026-03-01,acme,SKU-1001,5,66.92 +5004,2026-03-01,acme,SKU-1002,4,54.51 +5005,2026-03-01,acme,SKU-1001,4,229.28 +5006,2026-03-01,acme,SKU-3300,5,57.65 +5007,2026-03-01,acme,SKU-1002,2,47.54 +5008,2026-03-01,acme,SKU-2040,2,75.15 +5009,2026-03-01,acme,SKU-1002,2,55.98 +5010,2026-03-01,acme,SKU-1001,5,202.85 +5011,2026-03-02,initech,SKU-2040,5,163.73 +5012,2026-03-02,globex,SKU-2040,4,86.68 +5013,2026-03-02,acme,SKU-1001,2,76.59 +5014,2026-03-02,initech,SKU-2040,5,231.01 +5015,2026-03-02,acme,SKU-1001,3,105.33 +5016,2026-03-02,globex,SKU-1002,2,113.19 +5017,2026-03-02,acme,SKU-1001,4,191.97 +5018,2026-03-02,globex,SKU-2040,5,222.30 +5019,2026-03-02,initech,SKU-3300,5,62.53 +5020,2026-03-02,globex,SKU-3300,1,44.55 +5021,2026-03-02,acme,SKU-2040,1,42.00 +5022,2026-03-03,initech,SKU-2040,3,30.45 +5023,2026-03-03,globex,SKU-1002,4,160.63 +5024,2026-03-03,acme,SKU-1002,4,192.72 +5025,2026-03-03,acme,SKU-3300,2,57.88 +5026,2026-03-03,acme,SKU-1002,4,127.63 +5027,2026-03-03,globex,SKU-1002,5,253.92 +5028,2026-03-03,globex,SKU-3300,5,296.55 +5029,2026-03-03,acme,SKU-1002,4,52.93 +5030,2026-03-03,acme,SKU-1002,2,19.23 +5031,2026-03-03,acme,SKU-2040,5,116.89 +5032,2026-03-03,initech,SKU-2040,2,80.20 +5033,2026-03-04,acme,SKU-3300,2,109.75 +5034,2026-03-04,initech,SKU-3300,5,146.74 +5035,2026-03-04,initech,SKU-3300,1,12.17 +5036,2026-03-04,acme,SKU-3300,1,17.28 +5037,2026-03-04,acme,SKU-1001,3,27.04 +5038,2026-03-04,acme,SKU-2040,2,80.60 +5039,2026-03-04,acme,SKU-3300,1,16.58 +5040,2026-03-04,globex,SKU-2040,3,99.55 +5041,2026-03-04,initech,SKU-3300,1,33.50 +5042,2026-03-04,acme,SKU-1002,3,42.63 diff --git a/evals/airflow/cases/debugging-manual-trigger-context/fixture/requirements.txt b/evals/airflow/cases/debugging-manual-trigger-context/fixture/requirements.txt new file mode 100644 index 0000000..b1dd561 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==3.3.2 diff --git a/evals/airflow/cases/debugging-manual-trigger-context/grade.py b/evals/airflow/cases/debugging-manual-trigger-context/grade.py new file mode 100644 index 0000000..a05e6e5 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/grade.py @@ -0,0 +1,178 @@ +"""Grader for debugging-manual-trigger-context. + +The fixture DAG breaks on Airflow 3.3 in three runtime-only ways: a removed +context key (`execution_date`), `context["logical_date"]` on manual runs that +have no logical date, and a `{{ ds_nodash }}` template that is undefined on +those runs. The grader executes the DAG twice in separate metadata DBs: + +- a dated run (`airflow dags test partner_feed 2026-03-02`), which must produce + the 2026-03-02 feed; +- a manual run with no logical date (`dag.test(logical_date=None, + run_after=2026-03-03T00:30Z)`), which must produce the feed for the day it + was triggered for (2026-03-03, UTC) and nothing else. The run is executed as + if it had been cleared and re-run the next day (its DagRun ``start_date`` is + 2026-03-04), so the wall clock and ``start_date`` cannot stand in for the + trigger day. + +Both runs use a US local timezone (``TZ``), so a day taken in local time +instead of UTC lands on the wrong date. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "partner_feed" +DATED_DAY = "2026-03-02" +MANUAL_RUN_AFTER = "2026-03-03T00:30:00+00:00" +# Start of the "cleared and re-run tomorrow" execution of the manual run. +RERUN_START = "2026-03-04T09:00:00+00:00" +# Worker local time; 00:30 UTC on 03-03 is still 03-02 here. +LOCAL_TZ = {"TZ": "America/Los_Angeles"} +MANUAL_DAY = "2026-03-03" +CHAIN = ["extract_orders", "write_manifest", "deliver"] +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def expected_rows(day: str) -> list[dict]: + with (CASE_DIR / "fixture" / "data" / "partner_orders.csv").open(newline="") as fh: + return [{c: r[c] for c in FEED_COLUMNS} for r in csv.DictReader(fh) + if r["order_date"] == day and r["partner"] == "acme"] + + +def read_csv(path: Path): + if not path.is_file(): + return f"missing {path.name}" + with path.open(newline="") as fh: + return [{c: (r.get(c) or "").strip() for c in FEED_COLUMNS} for r in csv.DictReader(fh)] + + +def read_manifest(path: Path): + if not path.is_file(): + return f"missing {path.name}" + try: + data = json.loads(path.read_text()) + except ValueError as exc: + return f"invalid json: {exc}" + return {"partition": data.get("partition"), "row_count": data.get("row_count")} + + +def check_outputs(ws: Path, day: str) -> tuple[bool, str]: + """Feed, manifest and outbox copies for ``day`` are correct and no other day was produced.""" + rows = expected_rows(day) + want_manifest = {"partition": day, "row_count": len(rows)} + stamp = day.replace("-", "") + feed_dir = ws / "output" / "partner_feed" + outbox = ws / "outbox" / "acme" + got = { + "feed": read_csv(feed_dir / day / "orders.csv"), + "manifest": read_manifest(feed_dir / day / "manifest.json"), + "outbox_feed": read_csv(outbox / f"acme_orders_{stamp}.csv"), + "outbox_manifest": read_manifest(outbox / f"acme_orders_{stamp}.manifest.json"), + } + problems = [k for k, want in (("feed", rows), ("manifest", want_manifest), + ("outbox_feed", rows), ("outbox_manifest", want_manifest)) + if got[k] != want] + other_days = sorted(p.name for p in feed_dir.iterdir() if p.name != day) if feed_dir.is_dir() else [] + other_files = sorted(p.name for p in outbox.iterdir() if stamp not in p.name) if outbox.is_dir() else [] + if other_days or other_files: + problems.append(f"outputs for other days: {other_days + other_files}") + detail = "ok" if not problems else ( + f"problems={problems}; expected {len(rows)} rows + manifest {want_manifest}; " + f"got {json.dumps(got)[:1500]}") + return not problems, detail + + +def clean_outputs(ws: Path) -> None: + for sub in ("output", "outbox"): + shutil.rmtree(ws / sub, ignore_errors=True) + + +def reachable(dag: dict, src: str, dst: str) -> bool: + details, seen, stack = dag["task_details"], set(), [src] + while stack: + for nxt in details.get(stack.pop(), {}).get("downstream", []): + if nxt == dst: + return True + if nxt not in seen: + seen.add(nxt) + stack.append(nxt) + return False + + +MANUAL_PROBE = """ +import pendulum +from unittest import mock +import airflow.models.dagrun as dagrun_mod +dag = get_dag({dag_id!r}) +_create = dagrun_mod.get_or_create_dagrun +def _rerun_next_day(**kw): + # dag.test() starts the run at run_after; a run cleared and re-run the next + # day starts later, so its start_date no longer matches the trigger time. + kw["start_date"] = pendulum.parse({rerun_start!r}) + return _create(**kw) +with mock.patch.object(dagrun_mod, "get_or_create_dagrun", _rerun_next_day): + dr = dag.test(logical_date=None, run_after=pendulum.parse({run_after!r})) +RESULT = {{"state": str(getattr(dr.state, "value", dr.state)), + "logical_date": None if dr.logical_date is None else dr.logical_date.isoformat(), + "run_after": dr.run_after.isoformat()}} +""" + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + imp = g.import_dags(py, ws) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + dag = imp["dags"].get(DAG_ID) + grader.primary(f"DAG {DAG_ID} is present", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is not None: + missing = [t for t in CHAIN if t not in dag["tasks"]] + broken = [f"{a}->{b}" for a, b in zip(CHAIN, CHAIN[1:]) + if a in dag["tasks"] and b in dag["tasks"] and not reachable(dag, a, b)] + grader.primary("task graph preserved (extract -> manifest -> deliver)", not missing and not broken, + f"missing={missing} broken={broken} deps={dag['deps']}") + + # Dated run: the logical date decides the day, not the wall clock. + clean_outputs(ws) + dated = g.run_dags_test(py, ws, DAG_ID, DATED_DAY, env=g.airflow_env(ws, extra=LOCAL_TZ)) + grader.primary(f"dags test with logical date {DATED_DAY} succeeds", dated.ok, + "" if dated.ok else f"failed tasks={dated.failed_tasks()}\n{dated.log[-3000:]}") + ok, detail = check_outputs(ws, DATED_DAY) + grader.primary(f"dated run delivers the {DATED_DAY} feed", ok, detail) + + # Manual run with no logical date, as the UI/API trigger creates in Airflow 3. + clean_outputs(ws) + env = g.airflow_env(ws, extra=LOCAL_TZ) + db = g.ensure_db(py, env) + res, proc = (None, db) if not db.ok else g.probe_json( + py, MANUAL_PROBE.format(dag_id=DAG_ID, run_after=MANUAL_RUN_AFTER, rerun_start=RERUN_START), ws, env=env) + runs = g.dag_runs(py, ws, DAG_ID, env) if db.ok else [] + failed = [t for r in runs for t, s in r.get("tasks", {}).items() if s != "success"] + manual_ok = (res is not None and res.get("state") == "success" and res.get("logical_date") is None + and not failed) + grader.primary("manual run without a logical date succeeds", manual_ok, + f"result={res} non-success tasks={failed}" + ("" if manual_ok else "\n" + proc.tail(40))) + ok, detail = check_outputs(ws, MANUAL_DAY) + grader.primary(f"manual run delivers the feed for its trigger day ({MANUAL_DAY})", ok, detail) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/data_interval_fallback/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/data_interval_fallback/dags/partner_feed.py new file mode 100644 index 0000000..ab87412 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/data_interval_fallback/dags/partner_feed.py @@ -0,0 +1,85 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + when = context["data_interval_start"] + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ data_interval_start | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ data_interval_start | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/local_tz_day/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/local_tz_day/dags/partner_feed.py new file mode 100644 index 0000000..eb65aaa --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/local_tz_day/dags/partner_feed.py @@ -0,0 +1,87 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + dag_run = context["dag_run"] + # Manual runs: the day it was triggered, in the worker's local time. + when = dag_run.logical_date or dag_run.run_after.astimezone() + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after.astimezone()) | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after.astimezone()) | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/now_fallback/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/now_fallback/dags/partner_feed.py new file mode 100644 index 0000000..906b50a --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/now_fallback/dags/partner_feed.py @@ -0,0 +1,86 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + when = context.get("logical_date") or pendulum.now("UTC") + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/run_after_always/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/run_after_always/dags/partner_feed.py new file mode 100644 index 0000000..d2f03b8 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/run_after_always/dags/partner_feed.py @@ -0,0 +1,86 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + dag_run = context["dag_run"] + when = dag_run.run_after + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ dag_run.run_after | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ dag_run.run_after | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/skip_manual_runs/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/skip_manual_runs/dags/partner_feed.py new file mode 100644 index 0000000..2567280 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/skip_manual_runs/dags/partner_feed.py @@ -0,0 +1,88 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task +from airflow.sdk.exceptions import AirflowSkipException + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + when = context.get("logical_date") + if when is None: + raise AirflowSkipException("manual run without a logical date: nothing to deliver") + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/start_date_fallback/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/start_date_fallback/dags/partner_feed.py new file mode 100644 index 0000000..8f24670 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/start_date_fallback/dags/partner_feed.py @@ -0,0 +1,86 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + dag_run = context["dag_run"] + when = dag_run.logical_date or dag_run.start_date + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.start_date) | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.start_date) | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/mutants/template_left/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/mutants/template_left/dags/partner_feed.py new file mode 100644 index 0000000..d90ea29 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/mutants/template_left/dags/partner_feed.py @@ -0,0 +1,86 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + dag_run = context["dag_run"] + when = dag_run.logical_date or dag_run.run_after + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-manual-trigger-context/reference/dags/partner_feed.py b/evals/airflow/cases/debugging-manual-trigger-context/reference/dags/partner_feed.py new file mode 100644 index 0000000..a8d0325 --- /dev/null +++ b/evals/airflow/cases/debugging-manual-trigger-context/reference/dags/partner_feed.py @@ -0,0 +1,86 @@ +"""Daily order feed for our partner ACME. + +For each day: extract ACME's orders, write a manifest next to them, then copy +both into the outbox that the partner's SFTP sync picks up. +""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "partner_orders.csv" +FEED_DIR = PROJECT_ROOT / "output" / "partner_feed" +OUTBOX = PROJECT_ROOT / "outbox" / "acme" +FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + + +def feed_day(context) -> str: + """Day (YYYY-MM-DD) this run produces the feed for. + + Scheduled runs use their logical date. Manual runs triggered without a + logical date have no `logical_date`/`ds` in the context; they use the time + the run was triggered for, so clearing the run later reproduces the same day. + """ + dag_run = context["dag_run"] + when = dag_run.logical_date or dag_run.run_after + return when.strftime("%Y-%m-%d") + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["partner", "acme"], +) +def partner_feed(): + @task + def extract_orders(**context) -> str: + day = feed_day(context) + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} + for row in csv.DictReader(fh) + if row["order_date"] == day and row["partner"] == "acme" + ] + out = FEED_DIR / day / "orders.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FEED_COLUMNS) + writer.writeheader() + writer.writerows(rows) + return str(out) + + @task + def write_manifest(feed_path: str, **context) -> str: + day = feed_day(context) + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") + manifest.write_text( + json.dumps({"partner": "acme", "partition": day, "row_count": row_count}, indent=2) + ) + return str(manifest) + + deliver = BashOperator( + task_id="deliver", + bash_command=( + "mkdir -p {{ params.outbox }}" + " && cp {{ ti.xcom_pull(task_ids='extract_orders') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.csv" + " && cp {{ ti.xcom_pull(task_ids='write_manifest') }}" + " {{ params.outbox }}/acme_orders_{{ (dag_run.logical_date or dag_run.run_after) | ds_nodash }}.manifest.json" + ), + params={"outbox": str(OUTBOX)}, + ) + + write_manifest(extract_orders()) >> deliver + + +partner_feed() diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/alt_reference/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/alt_reference/dags/warehouse_ingest.py new file mode 100644 index 0000000..f45363f --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/alt_reference/dags/warehouse_ingest.py @@ -0,0 +1,58 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +The table list is read from the deployed catalog snapshot (`config/tables.json`), +never from the catalog service, so the dag-processor can re-parse this file +cheaply and always gets the same DAG. Loads run one at a time because DuckDB +has a single writer. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import duckdb +import pendulum +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + + +def catalog_tables() -> list[str]: + """Tables in the deployed catalog snapshot, de-duplicated, in catalog order.""" + return list(dict.fromkeys(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +@dag( + dag_id="warehouse_ingest", + description="Load the raw catalog tables into the DuckDB warehouse", + schedule="30 1 * * *", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + tags=["warehouse", "ingest"], +) +def warehouse_ingest(): + @task + def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as select * from read_csv(?, header = true)", + [str(RAW_DIR / f"{table}.csv")], + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + return rows + + previous = EmptyOperator(task_id="start") + for table in catalog_tables(): + current = load_table.override(task_id=f"load_{table}")(table) + previous >> current + previous = current + previous >> EmptyOperator(task_id="done") + + +warehouse_ingest() diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/case.yaml b/evals/airflow/cases/debugging-nondeterministic-parse/case.yaml new file mode 100644 index 0000000..d684c5a --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/case.yaml @@ -0,0 +1,56 @@ +id: debugging-nondeterministic-parse +area: debugging +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + Something is off with warehouse_ingest since we moved to Airflow 3.3: + + - the version dropdown in the UI shows a new DAG version for it almost every + time the dag-processor runs (hundreds by now, and it's the only DAG doing that) + - in the graph view the load_* tasks show up in a different order between versions + - the dag-processor logs say dags/warehouse_ingest.py takes 4-5 seconds to + parse, and the platform team asked us to stop hitting their catalog service + from the parser, it gets a request from every parse on every processor + + The DAG warnings panel also shows this for the file: + + This Dag uses runtime-variable values in Dag construction. + It causes the Dag version to increase as values change on every Dag parse. + Line 65, Col 5 ... Issue: Don't use runtime-varying value as argument in Dag constructor + + Can you fix it so it stops creating new versions and parses fast? Keep one + load_ task per catalog table (on-call reads the grid by table name), + keep the loads running one after another (DuckDB only allows one writer), and + keep it nightly at 01:30 UTC. There's no scheduler running locally, only the + airflow CLI. +description: | + Debugging a DAG whose parse is slow and non-deterministic on Airflow 3.3, where + every serialization change creates a new DAG version. The fixture calls a + catalog HTTP service at module level (with retries and sleeps), iterates a + `set` of table names (order depends on PYTHONHASHSEED, so the chain order + changes per process), and uses `datetime.now()` in `start_date` and + `description`. Airflow's own inflation warning only points at the + `datetime.now()` arguments; the set ordering and the network call are hidden + layers. The deployed snapshot `config/tables.json` is the intended source. + Pain point: P3 (top-level code, parse cost, non-deterministic DAGs / Airflow 3 + versioning). + Primary checks: DAG files import; tasks start, done and load_
for all 8 + catalog tables with the loads strictly sequential (not mapped); four fresh + parses with different PYTHONHASHSEED and TZ (the last with the clock moved 3 + days 7 hours ahead) give the same Airflow `dag_hash`; + the file parses in <= 3.0s (grader's catalog stand-in answers after 4s); + parsing makes zero requests to the catalog; still 01:30 UTC daily (trigger or + data-interval timetable) with catchup off and a fixed past start_date; the + load tasks follow `config/tables.json` (a table added to a copy of the + snapshot, plus a duplicate entry, yields one more sequential load task); + `airflow dags test` succeeds and loads every raw table with the right row + counts. + Mutants: `set_order` (snapshot but unsorted `set`), `now_start_date` + (`datetime.now()` start_date left), `description_date` (date stamp left in the + description), `catalog_at_parse` (catalog still fetched at parse, 1s timeout, + sorted), `parallel_loads` (`start >> loads >> done`), `hardcoded_tables` + (table list pasted into the DAG file, no longer follows the catalog), + `utc_today_start_date` (`pendulum.today("UTC").add(days=-1)`, the `days_ago` + replacement: stable within a UTC day, new version every day). diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/README.md b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/README.md new file mode 100644 index 0000000..a3b31a4 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/README.md @@ -0,0 +1,11 @@ +# warehouse-dags + +Airflow 3.3 DAGs that load the nightly raw extracts (`data/raw/*.csv`) into the +DuckDB warehouse (`warehouse/warehouse.duckdb`). + +- `dags/warehouse_ingest.py`: one `load_
` task per table registered in + the platform team's catalog service (`CATALOG_URL`). +- `config/tables.json`: copy of the catalog that the deploy pipeline refreshes + on every release. + +Local run: `airflow dags test warehouse_ingest 2026-03-02`. diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/config/tables.json b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/config/tables.json new file mode 100644 index 0000000..b0a4b30 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/config/tables.json @@ -0,0 +1,10 @@ +[ + "customers", + "orders", + "order_items", + "payments", + "refunds", + "shipments", + "products", + "inventory" +] diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_ingest.py new file mode 100644 index 0000000..81feb3f --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_ingest.py @@ -0,0 +1,83 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog service. +DuckDB allows a single writer, so the loads run one after another between +`start` and `done`. +""" + +from __future__ import annotations + +import json +import os +import time +import urllib.request +from datetime import datetime, timedelta +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +CATALOG_URL = os.environ.get( + "CATALOG_URL", "http://localhost:8765/api/v1/tables?domain=warehouse" +) +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + + +def list_tables(retries: int = 3, backoff_s: float = 1.5) -> list[str]: + """Tables registered in the catalog for the warehouse domain. + + Retries with exponential backoff and falls back to the deployed snapshot + when the service is unreachable. + """ + for attempt in range(retries): + try: + with urllib.request.urlopen(CATALOG_URL, timeout=5) as resp: + return [t["name"] for t in json.load(resp)["tables"]] + except OSError: + if attempt < retries - 1: + time.sleep(backoff_s * 2**attempt) + return json.loads(CATALOG_SNAPSHOT.read_text()) + + +# The catalog sometimes lists a table twice. +TABLES = set(list_tables()) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables (catalog read {datetime.now():%Y-%m-%d %H:%M})", + schedule="30 1 * * *", + start_date=datetime.now() - timedelta(days=1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_maintenance.py b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_maintenance.py new file mode 100644 index 0000000..f9d9ba1 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/dags/warehouse_maintenance.py @@ -0,0 +1,32 @@ +"""Weekly maintenance of the DuckDB warehouse: checkpoint the WAL and reclaim space.""" + +from __future__ import annotations + +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import dag, task + +WAREHOUSE_DB = Path(__file__).resolve().parents[1] / "warehouse" / "warehouse.duckdb" + + +@dag( + schedule="0 3 * * 0", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "maintenance"], +) +def warehouse_maintenance(): + @task + def checkpoint() -> None: + if not WAREHOUSE_DB.exists(): + print("warehouse not created yet, nothing to do") + return + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute("checkpoint") + + checkpoint() + + +warehouse_maintenance() diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/customers.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/customers.csv new file mode 100644 index 0000000..f0c98cd --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/customers.csv @@ -0,0 +1,4 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,customers-563 +2,2026-03-02T01:00:00,customers-986 +3,2026-03-03T02:00:00,customers-673 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/inventory.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/inventory.csv new file mode 100644 index 0000000..bbe7925 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/inventory.csv @@ -0,0 +1,4 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,inventory-160 +2,2026-03-02T01:00:00,inventory-136 +3,2026-03-03T02:00:00,inventory-294 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/order_items.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/order_items.csv new file mode 100644 index 0000000..79141cc --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/order_items.csv @@ -0,0 +1,8 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,order_items-975 +2,2026-03-02T01:00:00,order_items-701 +3,2026-03-03T02:00:00,order_items-294 +4,2026-03-01T03:00:00,order_items-289 +5,2026-03-02T04:00:00,order_items-923 +6,2026-03-03T05:00:00,order_items-624 +7,2026-03-01T06:00:00,order_items-587 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/orders.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/orders.csv new file mode 100644 index 0000000..4ce6bd9 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/orders.csv @@ -0,0 +1,6 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,orders-977 +2,2026-03-02T01:00:00,orders-899 +3,2026-03-03T02:00:00,orders-576 +4,2026-03-01T03:00:00,orders-562 +5,2026-03-02T04:00:00,orders-620 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/payments.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/payments.csv new file mode 100644 index 0000000..0e571d2 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/payments.csv @@ -0,0 +1,10 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,payments-744 +2,2026-03-02T01:00:00,payments-728 +3,2026-03-03T02:00:00,payments-912 +4,2026-03-01T03:00:00,payments-290 +5,2026-03-02T04:00:00,payments-196 +6,2026-03-03T05:00:00,payments-557 +7,2026-03-01T06:00:00,payments-410 +8,2026-03-02T07:00:00,payments-245 +9,2026-03-03T08:00:00,payments-192 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/products.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/products.csv new file mode 100644 index 0000000..1f44f32 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/products.csv @@ -0,0 +1,9 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,products-730 +2,2026-03-02T01:00:00,products-765 +3,2026-03-03T02:00:00,products-261 +4,2026-03-01T03:00:00,products-738 +5,2026-03-02T04:00:00,products-115 +6,2026-03-03T05:00:00,products-951 +7,2026-03-01T06:00:00,products-641 +8,2026-03-02T07:00:00,products-164 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/refunds.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/refunds.csv new file mode 100644 index 0000000..1b8d897 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/refunds.csv @@ -0,0 +1,5 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,refunds-651 +2,2026-03-02T01:00:00,refunds-929 +3,2026-03-03T02:00:00,refunds-810 +4,2026-03-01T03:00:00,refunds-749 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/shipments.csv b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/shipments.csv new file mode 100644 index 0000000..0d19748 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/data/raw/shipments.csv @@ -0,0 +1,7 @@ +id,updated_at,payload +1,2026-03-01T00:00:00,shipments-142 +2,2026-03-02T01:00:00,shipments-709 +3,2026-03-03T02:00:00,shipments-505 +4,2026-03-01T03:00:00,shipments-563 +5,2026-03-02T04:00:00,shipments-769 +6,2026-03-03T05:00:00,shipments-856 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/fixture/requirements.txt b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/requirements.txt new file mode 100644 index 0000000..e5feecf --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +duckdb==1.5.6 diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/grade.py b/evals/airflow/cases/debugging-nondeterministic-parse/grade.py new file mode 100644 index 0000000..0ee5165 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/grade.py @@ -0,0 +1,331 @@ +"""Grader for debugging-nondeterministic-parse. + +The fixture DAG builds its task list from a slow HTTP catalog call at module +level, iterates a `set` (order depends on the per-process hash seed), and puts +`datetime.now()` in `start_date` and `description`. On Airflow 3 every parse +serializes differently, so the dag-processor writes a new DAG version on every +parse, and each parse waits on the catalog service. + +The grader runs its own slow catalog service (it counts requests) and parses +the dags folder in four fresh processes, each with a different +PYTHONHASHSEED and local timezone (TZ), the last one with the clock moved +ahead by a few days. A fixed DAG serializes to the same +Airflow `dag_hash` every time, parses under the time budget and never calls +the catalog while parsing. It must also keep one `load_
` task per +catalog table, run the loads one at a time, keep the 01:30 UTC schedule, and +load every table when run. The task list must keep following the deployed +catalog snapshot (`config/tables.json`): a table added to the snapshot gets its +own load task without editing the DAG file. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +import threading +import time +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "warehouse_ingest" +# Table the grader adds to the catalog snapshot to check the DAG follows it. +NEW_TABLE = "returns" +# Below the stand-in catalog delay, with headroom for a loaded grading machine. +PARSE_BUDGET_S = 3.0 +CATALOG_DELAY_S = 4.0 +# (PYTHONHASHSEED, TZ, clock shift in seconds) per parse. The seeds give three +# different `set` orders for the catalog tables; the zones are 26 hours apart, +# so any local-time `now()`/`today()` differs even at day resolution. The last +# parse runs with the clock moved 3 days 7 hours ahead, so a UTC day-anchored +# value (`pendulum.today("UTC")`, the `days_ago` replacement) differs too. +CLOCK_SHIFT_S = str(3 * 86400 + 7 * 3600) +PARSE_SCENARIOS = [("1", "UTC", "0"), ("2", "Pacific/Kiritimati", "0"), ("3", "Etc/GMT+12", "0"), + ("4", "UTC", CLOCK_SHIFT_S)] + + +def catalog_tables() -> list[str]: + """Tables in the pristine catalog snapshot, de-duplicated.""" + return list(dict.fromkeys(json.loads((CASE_DIR / "fixture" / "config" / "tables.json").read_text()))) + + +def expected_row_counts() -> dict[str, int]: + out = {} + for table in catalog_tables(): + with (CASE_DIR / "fixture" / "data" / "raw" / f"{table}.csv").open(newline="") as fh: + out[table] = sum(1 for _ in csv.DictReader(fh)) + return out + + +class CatalogService: + """Slow local stand-in for the platform team's catalog service.""" + + def __init__(self) -> None: + tables = catalog_tables() + self.requests = 0 + self.lock = threading.Lock() + service = self + + class Handler(BaseHTTPRequestHandler): + def do_GET(self): # noqa: N802 + with service.lock: + service.requests += 1 + n = service.requests + time.sleep(CATALOG_DELAY_S) + # Rotate the order and repeat one table, like the real service. + names = tables[n % len(tables):] + tables[: n % len(tables)] + [tables[0]] + body = json.dumps({"tables": [{"name": t} for t in names]}).encode() + try: + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + except OSError: + pass # client gave up (timeout) + + def log_message(self, *args): + pass + + self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self.server.daemon_threads = True + self.url = f"http://127.0.0.1:{self.server.server_address[1]}/api/v1/tables?domain=warehouse" + threading.Thread(target=self.server.serve_forever, daemon=True).start() + + def count(self) -> int: + with self.lock: + return self.requests + + def close(self) -> None: + self.server.shutdown() + self.server.server_close() + + +PARSE_PROBE = """ +import json, os +import datetime as _dtmod +import duckdb, pendulum # noqa: F401 (bind the real datetime classes before any clock shift) +import airflow.sdk # noqa: F401 +import airflow.providers.standard.operators.empty, airflow.providers.standard.operators.python # noqa: F401 +from airflow.dag_processing.dagbag import DagBag # noqa: F401 +from airflow.models.serialized_dag import SerializedDagModel +from airflow.serialization.serialized_objects import DagSerialization + +_shift = _dtmod.timedelta(seconds=float(os.environ.get("EVAL_CLOCK_SHIFT_S", "0"))) +_real_dt, _real_date = _dtmod.datetime, _dtmod.date +if _shift: + # Parse as if the dag-processor ran later: datetime/date/pendulum "now" and + # "today" move by the shift. Constructed values stay real datetime objects. + class _Meta(type): + def __instancecheck__(cls, obj): + return isinstance(obj, cls._real) + + def __subclasscheck__(cls, sub): + return issubclass(sub, cls._real) + + class _ShiftedDatetime(_real_dt, metaclass=_Meta): + _real = _real_dt + + def __new__(cls, *args, **kwargs): + return _real_dt(*args, **kwargs) + + @classmethod + def now(cls, tz=None): + return _real_dt.now(tz) + _shift + + @classmethod + def utcnow(cls): + return _real_dt.utcnow() + _shift + + @classmethod + def today(cls): + return _real_dt.today() + _shift + + class _ShiftedDate(_real_date, metaclass=_Meta): + _real = _real_date + + def __new__(cls, *args, **kwargs): + return _real_date(*args, **kwargs) + + @classmethod + def today(cls): + return (_real_dt.now() + _shift).date() + + _dtmod.datetime, _dtmod.date = _ShiftedDatetime, _ShiftedDate +try: + bag = dagbag() +finally: + _dtmod.datetime, _dtmod.date = _real_dt, _real_date +dag = bag.dags.get({dag_id!r}) +if dag is None: + RESULT = {{"error": f"DAG missing; import errors: {{bag.import_errors}}"}} +else: + data = DagSerialization.to_dict(dag) + data = SerializedDagModel._sort_serialized_dag_dict(data) + data["dag"].pop("fileloc", None) + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + durations = [s.duration.total_seconds() for s in bag.dagbag_stats + if os.path.realpath(os.path.join(folder, s.file)) == os.path.realpath(dag.fileloc)] + RESULT = {{"hash": SerializedDagModel.hash(DagSerialization.to_dict(dag)), + "dag": data["dag"], + "parse_s": max(durations) if durations else None, + "stats": [[s.file, s.duration.total_seconds()] for s in bag.dagbag_stats]}} +""" + + +def differing_fields(dags: list[dict]) -> list[str]: + keys = sorted(set().union(*(d.keys() for d in dags))) + return [k for k in keys if len({json.dumps(d.get(k), sort_keys=True, default=str) for d in dags}) > 1] + + +def reachable(details: dict, src: str, dst: str) -> bool: + seen, stack = set(), [src] + while stack: + for nxt in details.get(stack.pop(), {}).get("downstream", []): + if nxt == dst: + return True + if nxt not in seen: + seen.add(nxt) + stack.append(nxt) + return False + + +def check_graph(dag: dict, loads: list[str] | None = None) -> tuple[bool, str]: + details = dag["task_details"] + loads = loads or [f"load_{t}" for t in catalog_tables()] + missing = [t for t in ["start", "done", *loads] if t not in details] + if missing: + return False, f"missing tasks {missing}; have {dag['tasks']}" + mapped = [t for t in loads if details[t]["mapped"]] + problems = [] + if mapped: + problems.append(f"mapped load tasks {mapped}") + problems += [f"start does not precede {t}" for t in loads if not reachable(details, "start", t)] + problems += [f"{t} does not precede done" for t in loads if not reachable(details, t, "done")] + parallel = [f"{a}|{b}" for i, a in enumerate(loads) for b in loads[i + 1:] + if not (reachable(details, a, b) or reachable(details, b, a))] + if parallel: + problems.append(f"loads that can run at the same time: {parallel[:6]}") + return not problems, "ok" if not problems else "; ".join(problems) + f" deps={dag['deps']}" + + +def follows_snapshot(py: str, ws: Path, extra: dict) -> tuple[bool, str]: + """Add a table (and a duplicate entry) to a copy of the workspace's snapshot and re-parse.""" + copy = g.copy_workspace(ws) + try: + snapshot = copy / "config" / "tables.json" + if not snapshot.is_file(): + return False, "config/tables.json is missing" + tables = json.loads(snapshot.read_text()) + [NEW_TABLE, "orders"] + snapshot.write_text(json.dumps(tables, indent=2)) + imp = g.import_dags(py, copy, env=g.airflow_env(copy, extra=extra), timeout=300) + dag = imp["dags"].get(DAG_ID) + if dag is None: + return False, f"DAG missing after adding {NEW_TABLE}: {imp['probe_error'] or imp['import_errors']}" + ok, detail = check_graph(dag, [f"load_{t}" for t in dict.fromkeys(tables)]) + return ok, f"snapshot + {NEW_TABLE} and a duplicate entry: {detail}" + finally: + shutil.rmtree(copy.parent, ignore_errors=True) + + +def read_counts(py: str, ws: Path, db: Path): + if not db.exists(): + return f"missing {db.relative_to(ws)}" + res, proc = g.probe_json(py, f""" + import duckdb + con = duckdb.connect({str(db)!r}, read_only=True) + names = [r[0] for r in con.execute("select table_name from information_schema.tables").fetchall()] + RESULT = {{n[4:]: con.execute(f'select count(*) from "{{n}}"').fetchone()[0] + for n in names if n.startswith("raw_")}} + """, ws) + return res if res is not None else f"probe failed: {proc.tail(5)}" + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + catalog = CatalogService() + extra = {"CATALOG_URL": catalog.url} + try: + imp = g.import_dags(py, ws, env=g.airflow_env(ws, extra=extra), timeout=300) + grader.primary("DAG files import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + dag = imp["dags"].get(DAG_ID) + grader.primary(f"DAG {DAG_ID} is present", dag is not None, f"found: {sorted(imp['dags'])}") + if dag is not None: + ok, detail = check_graph(dag) + grader.primary("one load_
task per catalog table, loads run one at a time", ok, detail) + + # Fresh parses: different hash seed, local timezone and clock each time. + parses = [] + for seed, tz, shift in PARSE_SCENARIOS: + env = g.airflow_env(ws, extra={**extra, "PYTHONHASHSEED": seed, "TZ": tz, + "EVAL_CLOCK_SHIFT_S": shift}) + res, proc = g.probe_json(py, PARSE_PROBE.format(dag_id=DAG_ID), ws, env=env, timeout=300) + parses.append(res if isinstance(res, dict) else {"error": proc.tail(15)}) + errors = [p["error"] for p in parses if "error" in p] + hashes = [p.get("hash") for p in parses] + same = not errors and len(set(hashes)) == 1 + detail = (f"probe errors: {errors}" if errors else + f"dag_hash per parse={hashes}; fields that differ: " + f"{differing_fields([p['dag'] for p in parses])}") + grader.primary("every parse serializes to the same DAG (no new version per parse)", same, detail) + + times = [p.get("parse_s") for p in parses] + fast = not errors and all(t is not None and t <= PARSE_BUDGET_S for t in times) + grader.primary(f"DAG file parses in <= {PARSE_BUDGET_S}s", fast, + f"parse seconds={times}" + (f" errors={errors}" if errors else "")) + + calls = catalog.count() + grader.primary("parsing never calls the catalog service", calls == 0, + f"{calls} catalog requests during {1 + len(PARSE_SCENARIOS)} parses") + + sched, proc = g.probe_json(py, f""" + dag = get_dag({DAG_ID!r}) + RESULT = {{"runs": scheduled_intervals(dag, "2030-01-01T00:00:00+00:00", n=3), + "start_date": dag.start_date.isoformat() if dag.start_date else None, + "catchup": dag.catchup}} + """, ws, env=g.airflow_env(ws, extra=extra)) + now = datetime.now(timezone.utc).isoformat() + # Trigger timetable (runs at 01:30 from 01-01) or data-interval + # timetable (first run at the end of the 01-01 interval) are both fine. + ok = (sched is not None + and [r["run_after"] for r in sched["runs"]] in ( + [f"2030-01-0{d}T01:30:00+00:00" for d in (1, 2, 3)], + [f"2030-01-0{d}T01:30:00+00:00" for d in (2, 3, 4)]) + and sched["catchup"] is False + and sched["start_date"] is not None and sched["start_date"] <= now) + grader.primary("still daily at 01:30 UTC, catchup off, start_date fixed in the past", ok, + f"{sched}" if sched is not None else proc.tail(10)) + + ok, detail = follows_snapshot(py, ws, extra) + grader.primary("load tasks follow the deployed catalog snapshot (config/tables.json)", ok, detail) + + # Run it (no logical date: works for any fixed start_date in the past). + shutil.rmtree(ws / "warehouse", ignore_errors=True) + run = g.run_dags_test(py, ws, DAG_ID, env=g.airflow_env(ws, extra=extra), timeout=600) + grader.primary("airflow dags test succeeds", run.ok, + "" if run.ok else f"failed tasks={run.failed_tasks()}\n{run.log[-3000:]}") + got = read_counts(py, ws, ws / "warehouse" / "warehouse.duckdb") + want = expected_row_counts() + grader.primary("every catalog table loaded into the warehouse", got == want, + f"expected {want}, got {got}") + finally: + catalog.close() + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/catalog_at_parse/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/catalog_at_parse/dags/warehouse_ingest.py new file mode 100644 index 0000000..cc388b3 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/catalog_at_parse/dags/warehouse_ingest.py @@ -0,0 +1,78 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +import os +import urllib.request +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +CATALOG_URL = os.environ.get( + "CATALOG_URL", "http://localhost:8765/api/v1/tables?domain=warehouse" +) + + +def list_tables() -> list[str]: + """Catalog tables; one quick attempt, then the deployed snapshot.""" + try: + with urllib.request.urlopen(CATALOG_URL, timeout=1) as resp: + return [t["name"] for t in json.load(resp)["tables"]] + except OSError: + return json.loads(CATALOG_SNAPSHOT.read_text()) + + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(list_tables())) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/description_date/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/description_date/dags/warehouse_ingest.py new file mode 100644 index 0000000..8dd9359 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/description_date/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables (catalog as of {datetime.now():%Y-%m-%d})", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/hardcoded_tables/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/hardcoded_tables/dags/warehouse_ingest.py new file mode 100644 index 0000000..ac2dc3b --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/hardcoded_tables/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = ["customers", "inventory", "order_items", "orders", "payments", "products", "refunds", "shipments"] + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/now_start_date/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/now_start_date/dags/warehouse_ingest.py new file mode 100644 index 0000000..14c49ed --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/now_start_date/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime, timedelta +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime.now() - timedelta(days=1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/parallel_loads/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/parallel_loads/dags/warehouse_ingest.py new file mode 100644 index 0000000..ac3545c --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/parallel_loads/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + start >> loads >> done diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/set_order/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/set_order/dags/warehouse_ingest.py new file mode 100644 index 0000000..39fb0d2 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/set_order/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice. +TABLES = set(json.loads(CATALOG_SNAPSHOT.read_text())) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/mutants/utc_today_start_date/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/utc_today_start_date/dags/warehouse_ingest.py new file mode 100644 index 0000000..2bfe9cd --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/mutants/utc_today_start_date/dags/warehouse_ingest.py @@ -0,0 +1,63 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +import pendulum +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + # days_ago(1) replacement suggested by Airflow's deprecation warning. + start_date=pendulum.today("UTC").add(days=-1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-nondeterministic-parse/reference/dags/warehouse_ingest.py b/evals/airflow/cases/debugging-nondeterministic-parse/reference/dags/warehouse_ingest.py new file mode 100644 index 0000000..2194b82 --- /dev/null +++ b/evals/airflow/cases/debugging-nondeterministic-parse/reference/dags/warehouse_ingest.py @@ -0,0 +1,62 @@ +"""Nightly load of the raw extracts into the DuckDB warehouse. + +One load task per table registered in the platform team's catalog. The table +list comes from `config/tables.json`, the catalog snapshot the deploy pipeline +refreshes on every release, so parsing this file makes no network calls and +produces the same DAG every time. DuckDB allows a single writer, so the loads +run one after another between `start` and `done`. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, chain + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_ROOT / "data" / "raw" +WAREHOUSE_DB = PROJECT_ROOT / "warehouse" / "warehouse.duckdb" +# Copy of the catalog response, refreshed by the deploy pipeline on every release. +CATALOG_SNAPSHOT = PROJECT_ROOT / "config" / "tables.json" + +# The catalog sometimes lists a table twice; sort so the task order is stable. +TABLES = sorted(set(json.loads(CATALOG_SNAPSHOT.read_text()))) + + +def load_table(table: str) -> int: + WAREHOUSE_DB.parent.mkdir(parents=True, exist_ok=True) + source = RAW_DIR / f"{table}.csv" + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + f"create or replace table raw_{table} as " + f"select * from read_csv('{source}', header = true)" + ) + (rows,) = con.execute(f"select count(*) from raw_{table}").fetchone() + print(f"loaded {rows} rows into raw_{table}") + return rows + + +with DAG( + dag_id="warehouse_ingest", + description=f"Load {len(TABLES)} raw tables into the warehouse", + schedule="30 1 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse", "ingest"], +): + start = EmptyOperator(task_id="start") + done = EmptyOperator(task_id="done") + loads = [ + PythonOperator( + task_id=f"load_{table}", + python_callable=load_table, + op_kwargs={"table": table}, + ) + for table in TABLES + ] + chain(start, *loads, done) diff --git a/evals/airflow/cases/debugging-variable-at-parse/alt_reference/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/alt_reference/dags/warehouse_load.py new file mode 100644 index 0000000..f06f1c5 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/alt_reference/dags/warehouse_load.py @@ -0,0 +1,78 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +The warehouse directory comes from the `warehouse_path` Airflow Variable. It is +passed to the tasks as a Jinja template, so it is resolved when a task runs and +never while the file is parsed. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import CronDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" +WAREHOUSE_PATH = "{{ var.value.warehouse_path }}" + + +@dag( + dag_id="warehouse_load", + # Airflow 2 semantics: the 06:00 run covers the previous day's interval. + schedule=CronDataIntervalTimetable("0 6 * * *", timezone="UTC"), + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], warehouse_path: str, ds=None) -> int: + db = Path(warehouse_path) / "warehouse.duckdb" + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, warehouse_path: str, ds=None) -> None: + db = Path(warehouse_path) / "warehouse.duckdb" + with duckdb.connect(str(db), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + loaded = load_warehouse(aggregate_revenue(extract_orders()), warehouse_path=WAREHOUSE_PATH) + validate_load(loaded, warehouse_path=WAREHOUSE_PATH) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/case.yaml b/evals/airflow/cases/debugging-variable-at-parse/case.yaml new file mode 100644 index 0000000..7d5e2b3 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/case.yaml @@ -0,0 +1,48 @@ +id: debugging-variable-at-parse +area: debugging +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + I'm moving our warehouse_load DAG over from the old Airflow 2 box to our new + 3.3 setup and it won't even show up. `airflow dags list-import-errors --local` + gives me this for dags/warehouse_load.py: + + Traceback (most recent call last): + File ".../site-packages/airflow/sdk/definitions/variable.py", line 54, in get + return _get_variable(key, deserialize_json=deserialize_json) + File ".../site-packages/airflow/sdk/execution_time/context.py", line 353, in _get_variable + raise AirflowRuntimeError( + airflow.sdk.exceptions.AirflowRuntimeError: VARIABLE_NOT_FOUND: {'message': 'Variable warehouse_path not found'} + + Which makes no sense, because the variable is definitely there: + `airflow variables get warehouse_path` prints our warehouse directory. CI has + no variables configured at all and fails on the same file. + + Can you figure out what's going on and fix the DAG so it imports and actually + runs? The warehouse location has to keep coming from the `warehouse_path` + variable (it's different in every environment), and don't drop any of the + steps or change when it runs. +description: | + Debugging a DAG that fails to import on Airflow 3.3 with a confusing error. + The fixture reads `Variable.get("warehouse_path")` at module level. Outside a + task (CLI, CI, `dags test`) the Task SDK cannot reach Variables, so parsing + fails with VARIABLE_NOT_FOUND even when the Variable exists in the metadata + DB. A second, hidden layer only shows up after that fix: the DAG still passes + the removed `schedule_interval` argument (TypeError). Pain points: P3/P10 + top-level Variable access, removed Airflow 2 arguments, and verifying a fix + instead of stopping at the first error. + Primary checks: every DAG file imports with no Variables available; the + extract -> aggregate -> load -> validate chain is kept; the DAG still runs + daily at 06:00 UTC (trigger or data-interval timetable) with catchup off; + `airflow dags test` succeeds with `warehouse_path` stored in the metadata DB; + the revenue rows land in the DuckDB file under the directory that Variable + points to. + Mutants: `default_at_parse` (top-level `Variable.get` with a project-local + default), `schedule_dropped` (TypeError "fixed" by deleting the schedule), + `hardcoded_path` (Variable replaced by a hard-coded path), + `variable_in_dag_body` (`Variable.get` moved into the `@dag` function body, + which still runs at parse time), `daily_preset` (`schedule_interval` + translated to `@daily`, i.e. midnight), `catchup_enabled` (catchup turned on + while rewriting the decorator). diff --git a/evals/airflow/cases/debugging-variable-at-parse/fixture/README.md b/evals/airflow/cases/debugging-variable-at-parse/fixture/README.md new file mode 100644 index 0000000..38760a5 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/fixture/README.md @@ -0,0 +1,16 @@ +# analytics-dags + +Airflow 3.3 DAGs for the analytics team. + +- `dags/order_status_report.py`: daily order counts per status (CSV in `output/`). +- `dags/warehouse_load.py`: daily revenue per region into the DuckDB warehouse + (being moved over from the old Airflow 2 box). + +Airflow Variables (set per environment, e.g. `airflow variables set ...`): + +| Variable | Meaning | +|---|---| +| `warehouse_path` | directory holding `warehouse.duckdb` | + +Local development: `pip install -r requirements.txt`, then point +`AIRFLOW__CORE__DAGS_FOLDER` at `dags/`. diff --git a/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/order_status_report.py b/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/order_status_report.py new file mode 100644 index 0000000..f3a5d59 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/order_status_report.py @@ -0,0 +1,37 @@ +"""Daily count of orders per status, written as one CSV per day.""" + +from __future__ import annotations + +import csv +from collections import Counter +from datetime import datetime +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="0 5 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["reporting"], +) +def order_status_report(): + @task + def count_statuses(ds=None) -> str: + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + counts = Counter(r["status"] for r in csv.DictReader(fh) if r["order_date"] == ds) + out = PROJECT_ROOT / "output" / "order_status_report" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["status", "orders"]) + writer.writerows(sorted(counts.items())) + return str(out) + + count_statuses() + + +order_status_report() diff --git a/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/warehouse_load.py new file mode 100644 index 0000000..69902d5 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/fixture/dags/warehouse_load.py @@ -0,0 +1,76 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + +WAREHOUSE_DIR = Path(Variable.get("warehouse_path")) +WAREHOUSE_DB = WAREHOUSE_DIR / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule_interval="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + WAREHOUSE_DIR.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(WAREHOUSE_DB), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/fixture/data/orders.csv b/evals/airflow/cases/debugging-variable-at-parse/fixture/data/orders.csv new file mode 100644 index 0000000..88ac254 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/fixture/data/orders.csv @@ -0,0 +1,59 @@ +order_id,order_date,region,amount,status +1001,2026-03-01,amer,14.88,completed +1002,2026-03-01,amer,60.12,completed +1003,2026-03-01,apac,357.41,completed +1004,2026-03-01,emea,17.55,completed +1005,2026-03-01,amer,204.62,completed +1006,2026-03-01,amer,287.83,completed +1007,2026-03-01,emea,92.07,completed +1008,2026-03-01,amer,304.73,completed +1009,2026-03-01,emea,139.40,completed +1010,2026-03-01,emea,45.37,completed +1011,2026-03-01,emea,339.76,completed +1012,2026-03-01,amer,293.24,completed +1013,2026-03-01,emea,36.13,completed +1014,2026-03-01,apac,249.32,cancelled +1015,2026-03-01,apac,80.95,completed +1016,2026-03-01,apac,95.02,completed +1017,2026-03-01,amer,342.85,cancelled +1018,2026-03-02,emea,184.10,cancelled +1019,2026-03-02,amer,151.22,completed +1020,2026-03-02,emea,282.22,completed +1021,2026-03-02,amer,245.61,completed +1022,2026-03-02,apac,101.70,completed +1023,2026-03-02,emea,395.86,completed +1024,2026-03-02,apac,91.75,completed +1025,2026-03-02,amer,95.47,completed +1026,2026-03-02,emea,163.46,completed +1027,2026-03-02,apac,351.17,completed +1028,2026-03-02,apac,202.20,cancelled +1029,2026-03-02,apac,186.25,completed +1030,2026-03-02,amer,299.27,completed +1031,2026-03-02,apac,235.91,cancelled +1032,2026-03-02,emea,147.99,cancelled +1033,2026-03-03,apac,199.94,completed +1034,2026-03-03,amer,65.37,completed +1035,2026-03-03,apac,171.75,completed +1036,2026-03-03,emea,240.38,completed +1037,2026-03-03,emea,388.58,cancelled +1038,2026-03-03,amer,273.71,completed +1039,2026-03-03,apac,301.60,completed +1040,2026-03-03,emea,49.06,completed +1041,2026-03-03,emea,6.28,completed +1042,2026-03-03,apac,109.04,completed +1043,2026-03-03,amer,205.53,completed +1044,2026-03-03,apac,122.89,completed +1045,2026-03-03,apac,83.57,completed +1046,2026-03-04,apac,381.63,cancelled +1047,2026-03-04,amer,241.58,completed +1048,2026-03-04,amer,371.99,cancelled +1049,2026-03-04,emea,99.58,completed +1050,2026-03-04,apac,379.05,completed +1051,2026-03-04,emea,327.33,cancelled +1052,2026-03-04,apac,307.50,completed +1053,2026-03-04,emea,379.01,completed +1054,2026-03-04,apac,349.61,completed +1055,2026-03-04,amer,371.92,completed +1056,2026-03-04,apac,84.45,completed +1057,2026-03-04,apac,261.70,completed +1058,2026-03-04,apac,183.34,completed diff --git a/evals/airflow/cases/debugging-variable-at-parse/fixture/requirements.txt b/evals/airflow/cases/debugging-variable-at-parse/fixture/requirements.txt new file mode 100644 index 0000000..e5feecf --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +duckdb==1.5.6 diff --git a/evals/airflow/cases/debugging-variable-at-parse/grade.py b/evals/airflow/cases/debugging-variable-at-parse/grade.py new file mode 100644 index 0000000..fe9f788 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/grade.py @@ -0,0 +1,130 @@ +"""Grader for debugging-variable-at-parse. + +The fixture DAG reads an Airflow Variable at module level (fails to parse +outside the dag-processor with VARIABLE_NOT_FOUND, even when the Variable exists +in the metadata DB) and, behind that, still passes the removed +`schedule_interval` argument. A correct fix imports with no Variables at all, +keeps the task graph and the 06:00 daily schedule, and still takes the +warehouse location from the `warehouse_path` Variable at run time. +""" + +from __future__ import annotations + +import csv +import os +import shutil +import sys +import tempfile +from collections import defaultdict +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "warehouse_load" +DAY = "2026-03-02" +TASK_CHAIN = ["extract_orders", "aggregate_revenue", "load_warehouse", "validate_load"] + + +def expected_revenue(day: str) -> dict[str, float]: + """Ground truth from the pristine fixture data.""" + totals: dict[str, float] = defaultdict(float) + with (CASE_DIR / "fixture" / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["order_date"] == day and row["status"] == "completed": + totals[row["region"]] += float(row["amount"]) + return {k: round(v, 2) for k, v in sorted(totals.items())} + + +def reachable(dag: dict, src: str, dst: str) -> bool: + details = dag["task_details"] + seen, stack = set(), [src] + while stack: + cur = stack.pop() + for nxt in details.get(cur, {}).get("downstream", []): + if nxt == dst: + return True + if nxt not in seen: + seen.add(nxt) + stack.append(nxt) + return False + + +def read_warehouse(py: str, ws: Path, db: Path, day: str): + """Rows of daily_revenue for ``day`` as {region: revenue}, or None if absent.""" + if not db.exists(): + return None + res, proc = g.probe_json(py, f""" + import duckdb + con = duckdb.connect({str(db)!r}, read_only=True) + rows = con.execute("select region, cast(revenue as double) from daily_revenue " + "where cast(revenue_date as varchar) = ?", [{day!r}]).fetchall() + RESULT = {{r: round(v, 2) for r, v in sorted(rows)}} + """, ws) + return res if res is not None else f"probe failed: {proc.tail(5)}" + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + # 1. Parse with no Variables anywhere (like CI / a fresh dev box). + imp = g.import_dags(py, ws) + errors = "; ".join(f"{k}: {v.strip().splitlines()[-1]}" for k, v in imp["import_errors"].items()) + grader.primary("DAG files import with no Variables available", + imp["ok"] and not imp["import_errors"], imp["probe_error"] or errors) + dag = imp["dags"].get(DAG_ID) + grader.primary(f"DAG {DAG_ID} is present", dag is not None, f"found: {sorted(imp['dags'])}") + + if dag is not None: + missing = [t for t in TASK_CHAIN if t not in dag["tasks"]] + broken = [f"{a}->{b}" for a, b in zip(TASK_CHAIN, TASK_CHAIN[1:]) + if a in dag["tasks"] and b in dag["tasks"] and not reachable(dag, a, b)] + grader.primary("task graph preserved (extract -> aggregate -> load -> validate)", + not missing and not broken, f"missing={missing} broken={broken} deps={dag['deps']}") + + sched, proc = g.probe_json(py, f""" + dag = get_dag({DAG_ID!r}) + RESULT = scheduled_intervals(dag, "2026-03-01T00:00:00+00:00", n=3, catchup=True) + """, ws) + run_after = [r["run_after"] for r in (sched or [])] + # Either a trigger timetable (first run 03-01 06:00) or a data-interval + # timetable (first run at the end of the 03-01 interval) is fine. + days = [ra[:10] for ra in run_after] + ok = (all(ra.endswith("T06:00:00+00:00") for ra in run_after) + and days in (["2026-03-01", "2026-03-02", "2026-03-03"], + ["2026-03-02", "2026-03-03", "2026-03-04"]) + and dag["catchup"] is False) + grader.primary("still scheduled daily at 06:00 UTC, catchup off", ok, + f"run_after={run_after} timetable={dag['timetable']} catchup={dag['catchup']} " + + ("" if sched is not None else proc.tail(10))) + + # 2. Run with the Variable stored in the metadata DB, pointing at a fresh dir. + shutil.rmtree(ws / "output", ignore_errors=True) + warehouse_dir = Path(tempfile.mkdtemp(prefix="eval-warehouse-")) + env = g.airflow_env(ws) + db_ready = g.ensure_db(py, env) + setvar = g.run_cmd([str(Path(py).parent / "airflow"), "variables", "set", "warehouse_path", + str(warehouse_dir)], env=env, cwd=ws) + if not (db_ready.ok and setvar.ok): + grader.primary("airflow dags test succeeds with warehouse_path set", False, + db_ready.tail(10) + setvar.tail(10)) + else: + run = g.run_dags_test(py, ws, DAG_ID, DAY, env=env) + grader.primary("airflow dags test succeeds with warehouse_path set", run.ok, + "" if run.ok else f"failed tasks={run.failed_tasks()}\n{run.log[-3000:]}") + got = read_warehouse(py, ws, warehouse_dir / "warehouse.duckdb", DAY) + want = expected_revenue(DAY) + grader.primary("revenue loaded into the warehouse the Variable points to", got == want, + f"expected {want}, got {got} (warehouse_path={warehouse_dir})") + shutil.rmtree(warehouse_dir, ignore_errors=True) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/catchup_enabled/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/catchup_enabled/dags/warehouse_load.py new file mode 100644 index 0000000..6ff375d --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/catchup_enabled/dags/warehouse_load.py @@ -0,0 +1,83 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +def warehouse_db() -> Path: + """Path of the warehouse DuckDB file. + + The Variable is read inside tasks only. At module level it would run on every + parse and fail wherever the parser cannot reach Variables (CI, `dags test`). + """ + return Path(Variable.get("warehouse_path")) / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=True, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/daily_preset/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/daily_preset/dags/warehouse_load.py new file mode 100644 index 0000000..51049b2 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/daily_preset/dags/warehouse_load.py @@ -0,0 +1,83 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +def warehouse_db() -> Path: + """Path of the warehouse DuckDB file. + + The Variable is read inside tasks only. At module level it would run on every + parse and fail wherever the parser cannot reach Variables (CI, `dags test`). + """ + return Path(Variable.get("warehouse_path")) / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule="@daily", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/default_at_parse/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/default_at_parse/dags/warehouse_load.py new file mode 100644 index 0000000..2a03f25 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/default_at_parse/dags/warehouse_load.py @@ -0,0 +1,81 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +WAREHOUSE_DIR = Path(Variable.get("warehouse_path", default=str(PROJECT_ROOT / "warehouse"))) + + +def warehouse_db() -> Path: + return WAREHOUSE_DIR / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/hardcoded_path/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/hardcoded_path/dags/warehouse_load.py new file mode 100644 index 0000000..85d54ea --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/hardcoded_path/dags/warehouse_load.py @@ -0,0 +1,83 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +def warehouse_db() -> Path: + """Path of the warehouse DuckDB file. + + The Variable is read inside tasks only. At module level it would run on every + parse and fail wherever the parser cannot reach Variables (CI, `dags test`). + """ + return PROJECT_ROOT / "warehouse" / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/schedule_dropped/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/schedule_dropped/dags/warehouse_load.py new file mode 100644 index 0000000..9b8a38d --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/schedule_dropped/dags/warehouse_load.py @@ -0,0 +1,82 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +def warehouse_db() -> Path: + """Path of the warehouse DuckDB file. + + The Variable is read inside tasks only. At module level it would run on every + parse and fail wherever the parser cannot reach Variables (CI, `dags test`). + """ + return Path(Variable.get("warehouse_path")) / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/mutants/variable_in_dag_body/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/mutants/variable_in_dag_body/dags/warehouse_load.py new file mode 100644 index 0000000..1d68d86 --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/mutants/variable_in_dag_body/dags/warehouse_load.py @@ -0,0 +1,77 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + + +@dag( + dag_id="warehouse_load", + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + WAREHOUSE_DIR = Path(Variable.get("warehouse_path")) + WAREHOUSE_DB = WAREHOUSE_DIR / "warehouse.duckdb" + + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + WAREHOUSE_DIR.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE_DB)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(WAREHOUSE_DB), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/debugging-variable-at-parse/reference/dags/warehouse_load.py b/evals/airflow/cases/debugging-variable-at-parse/reference/dags/warehouse_load.py new file mode 100644 index 0000000..e57423c --- /dev/null +++ b/evals/airflow/cases/debugging-variable-at-parse/reference/dags/warehouse_load.py @@ -0,0 +1,83 @@ +"""Load daily completed-order revenue per region into the analytics warehouse. + +Ported from the old Airflow 2 deployment. The warehouse is a DuckDB file whose +directory differs per environment, so it comes from the `warehouse_path` +Airflow Variable. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime +from pathlib import Path + +import duckdb +from airflow.sdk import Variable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_CSV = PROJECT_ROOT / "data" / "orders.csv" + + +def warehouse_db() -> Path: + """Path of the warehouse DuckDB file. + + The Variable is read inside tasks only. At module level it would run on every + parse and fail wherever the parser cannot reach Variables (CI, `dags test`). + """ + return Path(Variable.get("warehouse_path")) / "warehouse.duckdb" + + +@dag( + dag_id="warehouse_load", + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=["warehouse"], +) +def warehouse_load(): + @task + def extract_orders(ds=None) -> list[dict]: + with ORDERS_CSV.open(newline="") as fh: + return [ + row + for row in csv.DictReader(fh) + if row["order_date"] == ds and row["status"] == "completed" + ] + + @task + def aggregate_revenue(orders: list[dict]) -> dict[str, float]: + totals: dict[str, float] = defaultdict(float) + for order in orders: + totals[order["region"]] += float(order["amount"]) + return {region: round(total, 2) for region, total in sorted(totals.items())} + + @task + def load_warehouse(revenue: dict[str, float], ds=None) -> int: + db = warehouse_db() + db.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(db)) as con: + con.execute( + "create table if not exists daily_revenue " + "(revenue_date date, region varchar, revenue decimal(12, 2))" + ) + con.execute("delete from daily_revenue where revenue_date = ?", [ds]) + con.executemany( + "insert into daily_revenue values (?, ?, ?)", + [(ds, region, amount) for region, amount in revenue.items()], + ) + return len(revenue) + + @task + def validate_load(expected_rows: int, ds=None) -> None: + with duckdb.connect(str(warehouse_db()), read_only=True) as con: + (loaded,) = con.execute( + "select count(*) from daily_revenue where revenue_date = ?", [ds] + ).fetchone() + if loaded != expected_rows: + raise ValueError(f"expected {expected_rows} rows for {ds}, found {loaded}") + + validate_load(load_warehouse(aggregate_revenue(extract_orders()))) + + +warehouse_load() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/sensors.py new file mode 100644 index 0000000..e000f13 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/sensors.py @@ -0,0 +1,68 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue, Variable +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list.""" + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + ti = context["ti"] + key = f"lake_wait_start__{ti.dag_id}__{ti.run_id}__{ti.task_id}__{ti.map_index}" + started = Variable.get(key, default=None) + if started is None: + started = datetime.now(timezone.utc).isoformat() + Variable.set(key, started) + left = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) - datetime.now(timezone.utc) + if left <= timedelta(0): + raise (AirflowSkipException if self.soft_fail else AirflowSensorTimeout)("wait budget used up") + self.defer( + trigger=PartitionPublishedTrigger(table=self.table, partition=self.partition, + lake_conn_id=self.lake_conn_id, poll_interval=self.poke_interval), + method_name="execute_complete", + # the scheduler fails the deferral at this absolute time, restart or not; + # BaseSensorOperator turns that into AirflowSensorTimeout (soft_fail -> skipped) + timeout=left, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + raise AirflowSensorTimeout(f"{self.table}/{self.partition} not published in time") + raise AirflowException(f"catalog API error for {self.table}/{self.partition}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/triggers.py new file mode 100644 index 0000000..9c9ac08 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/alt_reference/dags/lake/triggers.py @@ -0,0 +1,63 @@ +"""Trigger that waits for a lake partition to be published (alt fix: no trigger-side deadline).""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + +# Published partitions never change, so remember them and spare the catalog API. +_PUBLISHED: dict[tuple[str, str], dict] = {} + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED or the API fails. + + The sensor's timeout is enforced by the scheduler through ``defer(timeout=...)``; + ``timeout`` is kept for compatibility with stored trigger rows. + """ + + def __init__( + self, + table: str, + partition: str, + timeout: float | None = None, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.timeout = timeout + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "timeout": self.timeout, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + key = (self.table, self.partition) + hook = LakeCatalogHook(self.lake_conn_id) + while key not in _PUBLISHED: + try: + info = await asyncio.to_thread(hook.get_partition, self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + _PUBLISHED[key] = info + break + await asyncio.sleep(self.poll_interval) + yield TriggerEvent({"status": "published", "files": _PUBLISHED[key]["files"]}) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/case.yaml b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/case.yaml new file mode 100644 index 0000000..f03232a --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/case.yaml @@ -0,0 +1,72 @@ +id: hard-deferrable-debug-partition-sensor +area: debugging +suite: hard +split: holdout +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +grade_timeout_s: 1500 +prompt: | + Our deferrable PartitionPublishedSensor (dags/lake/: hook, trigger and sensor) + has caused seven incidents since we moved to Airflow 3.3. Please find the root + causes and fix them, keeping the sensor deferrable and its constructor arguments + unchanged (the DAGs in dags/ use them). + + 1. Whenever the catalog API slows down (it regularly takes 1-2 s per call), the + triggerer logs that its event loop was blocked, and deferred tasks of + unrelated DAGs resume minutes late. + 2. After Tuesday's deploy restarted the triggerer, sensors with timeout=2h kept + waiting; one waited more than 7 hours. + 3. When a sensor times out, the task is retried (our DAGs use retries=3) and + every retry waits the full timeout again. A timed-out sensor should fail for + good, or be skipped when soft_fail=True. + 4. During yesterday's catalog outage (HTTP 503 on every call) the sensors went + green and the loads ran with no files. An API error (5xx or a connection + error) should fail the current try, so the task's retries take over. + 5. orders_diff waits for today's and yesterday's orders partitions. Its "today" + and "yesterday" sensors often report the same file list. + 6. Since we made the sensor finish straight from the triggerer (to free worker + slots), load_orders logs absurd file counts ("loading 92 files into orders" + for a partition with 2 files). Every consumer reads the partition's file list + from the sensor's return value, so that value has to be the list of files. + 7. During Monday's 5-minute catalog outage the orders sensor failed, was retried, + and the retry waited another full 2 hours. `timeout` is meant as the total + wait for a partition in a DAG run, counted from when the sensor first started + waiting, retries included. + + README.md describes the catalog API; tools/ has a local stand-in for it. +description: | + Hard suite, theme deferrable, area debugging (Airflow 3.3). Seven production + incidents in a deferrable sensor + trigger + hook, each with a planted root cause. + Incident 7 (timeout restarts on every retry) needs a wait-start anchor that survives + retries: on 3.3 a retry clears the task instance's XComs and Variable.set() only + takes strings; the reference uses the 3.3 task state store + (context["task_state_store"]), alt_reference a string Variable. + Version-specific one: the trigger ends the task with + TaskSuccessEvent(xcoms={"return_value": files}); on Airflow 3.3.2 XComs pushed that + way (models/trigger.py handle_event_submit -> TaskInstance.xcom_push -> XComModel.set + json.dumps into a JSON column) reach downstream tasks as a JSON *string*, so loads + count characters. Plain `dags test` does not show it (it resumes the task on the + worker instead), so the fix is to resume on the worker and return the list. Others: + an `async def` hook method that calls `requests` (blocks the triggerer loop), a + trigger deadline computed from time.monotonic() at construction plus no + defer(timeout=) (a triggerer restart resets it), the timeout path raising a + retryable AirflowException, any non-timeout event (including errors) treated as + success, and a module-level cache of published partitions keyed by table only + (triggers share the triggerer process). Graded with grader-only DAGs against a + grader-owned catalog API (0.5 s per request) via grader_data/defer_runner.py + (deferral timeouts enforced with a 4 s grace, triggerer restarted mid-wait, + event-loop stalls measured). Primary checks: DAGs import; the sensor still + defers and its return value (read by a downstream task) is the published file + list; no event-loop stall >= 0.35 s; a + timed-out sensor (timeout=10 s, retries=2) fails on try 1; with a restart 8 s + into the wait it still fails 8.5-17 s after deferring; soft_fail sensor is + skipped; a 503 outage fails the sensor and the downstream load does not run; + budget scenario (6 s upstream task, timeout=16 s, retries=4, 503s from 3-8 s after + the first request) fails 14.5-21.5 s after the sensor's first start after >= 2 + tries; D and D-1 sensors on one table get their own files. + Mutants: each root cause left unfixed in turn (blocking hook, relative deadline + without defer timeout, retryable timeout, errors as success, table-keyed cache, + TaskSuccessEvent return value, timeout budget restarted on each try), API errors + polled through instead of failing the try, and a trigger that fabricates the file + list without calling the catalog. diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/.gitignore b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/.gitignore new file mode 100644 index 0000000..5b478e6 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.log diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/README.md b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/README.md new file mode 100644 index 0000000..5218d94 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/README.md @@ -0,0 +1,25 @@ +# lake-pipelines + +Airflow 3.3 DAGs that load data-lake partitions once the catalog marks them published. + +- `dags/lake/` — `LakeCatalogHook`, `PartitionPublishedTrigger`, `PartitionPublishedSensor`. +- `dags/lake_ingest.py`, `dags/orders_diff.py` — the DAGs using the sensor. +- `tools/catalog_stub.py` — local stand-in for the catalog API (run it with `--help`). + +## Catalog API + +Connection `lake_default` (type `http`): `host`, `port`, `schema` = URL scheme, +`password` = bearer token. + +`GET /v1/tables/
/partitions/` answers +`200 {"state": "PENDING"}` or `200 {"state": "PUBLISHED", "files": [...]}`. +Published partitions never change. Under load the API takes 1-2 s per call and +sometimes answers `503`. + +## Local development + +```bash +python tools/catalog_stub.py --publish orders/2026-09-29=0 & # prints its port +export AIRFLOW_CONN_LAKE_DEFAULT='{"conn_type": "http", "host": "127.0.0.1", "port": , "password": "eval-lake-token"}' +airflow dags test lake_ingest 2026-09-29 +``` diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/.airflowignore b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/.airflowignore new file mode 100644 index 0000000..35012b5 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/.airflowignore @@ -0,0 +1 @@ +lake/ diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/__init__.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/hooks.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/hooks.py new file mode 100644 index 0000000..4d1973f --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/hooks.py @@ -0,0 +1,54 @@ +"""Hook for the data-lake catalog API. + +GET /v1/tables/
/partitions/ + 200 {"state": "PENDING"} or {"state": "PUBLISHED", "files": ["s3://...", ...]} + anything else is an error +""" + +from __future__ import annotations + +import requests +from airflow.sdk import BaseHook + + +class LakeApiError(Exception): + """The catalog API answered with an error or could not be reached.""" + + +class LakeCatalogHook(BaseHook): + """Connection: ``host``/``port`` locate the API, ``schema`` is the URL scheme, ``password`` the token.""" + + conn_name_attr = "lake_conn_id" + default_conn_name = "lake_default" + conn_type = "http" + hook_name = "Lake catalog" + + def __init__(self, lake_conn_id: str = default_conn_name, request_timeout: float = 20.0) -> None: + super().__init__() + self.lake_conn_id = lake_conn_id + self.request_timeout = request_timeout + + @staticmethod + def _url_and_headers(conn, table: str, partition: str) -> tuple[str, dict[str, str]]: + port = f":{conn.port}" if conn.port else "" + url = f"{conn.schema or 'http'}://{conn.host}{port}/v1/tables/{table}/partitions/{partition}" + return url, {"Authorization": f"Bearer {conn.password}"} + + def _fetch(self, url: str, headers: dict[str, str]) -> dict: + try: + resp = requests.get(url, headers=headers, timeout=self.request_timeout) + except requests.RequestException as exc: + raise LakeApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise LakeApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() + + def get_partition(self, table: str, partition: str) -> dict: + url, headers = self._url_and_headers(self.get_connection(self.lake_conn_id), table, partition) + return self._fetch(url, headers) + + async def aget_partition(self, table: str, partition: str) -> dict: + """Async variant for triggers.""" + conn = await self.aget_connection(self.lake_conn_id) + url, headers = self._url_and_headers(conn, table, partition) + return self._fetch(url, headers) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/sensors.py new file mode 100644 index 0000000..7966da3 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/sensors.py @@ -0,0 +1,66 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + timeout=self.timeout, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "timeout": + raise AirflowException(f"{self.table}/{self.partition} was not published within {self.timeout}s") + files = event.get("files", []) + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/triggers.py new file mode 100644 index 0000000..e9c354a --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake/triggers.py @@ -0,0 +1,66 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +import time +from typing import Any + +from airflow.triggers.base import BaseTrigger, TaskSuccessEvent, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + +# Published partitions never change, so remember them and spare the catalog API. +_PUBLISHED: dict[str, dict] = {} + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``timeout`` seconds pass.""" + + def __init__( + self, + table: str, + partition: str, + timeout: float, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.timeout = timeout + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + self.give_up_at = time.monotonic() + timeout + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "timeout": self.timeout, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + if self.table in _PUBLISHED: + # Finish the task right here: no worker slot is needed just to return the list. + yield TaskSuccessEvent(xcoms={"return_value": _PUBLISHED[self.table]["files"]}) + return + if time.monotonic() >= self.give_up_at: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + _PUBLISHED[self.table] = info + continue + await asyncio.sleep(self.poll_interval) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake_ingest.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake_ingest.py new file mode 100644 index 0000000..84be587 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/lake_ingest.py @@ -0,0 +1,35 @@ +"""Daily ingest of lake partitions into the warehouse.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +from airflow.sdk import dag, task + +from lake.sensors import PartitionPublishedSensor + + +@dag( + schedule="0 5 * * *", + start_date=datetime(2026, 9, 1), + catchup=False, + default_args={"retries": 3, "retry_delay": timedelta(minutes=5)}, + tags=["lake"], +) +def lake_ingest(): + orders = PartitionPublishedSensor(task_id="wait_orders", table="orders", poke_interval=120, timeout=2 * 3600) + # customers is best effort: skip the load when the partition is late + customers = PartitionPublishedSensor( + task_id="wait_customers", table="customers", poke_interval=120, timeout=2 * 3600, soft_fail=True + ) + + @task + def load(table: str, files: list[str]): + print(f"loading {len(files)} files into {table}") + return len(files) + + load.override(task_id="load_orders")("orders", orders.output) + load.override(task_id="load_customers")("customers", customers.output) + + +lake_ingest() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/orders_diff.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/orders_diff.py new file mode 100644 index 0000000..faa0a9a --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/dags/orders_diff.py @@ -0,0 +1,34 @@ +"""Day-over-day diff of the orders partition.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +from airflow.sdk import dag, task + +from lake.sensors import PartitionPublishedSensor + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 9, 1), + catchup=False, + default_args={"retries": 3, "retry_delay": timedelta(minutes=5)}, + tags=["lake"], +) +def orders_diff(): + today = PartitionPublishedSensor(task_id="wait_today", table="orders", partition="{{ ds }}", + poke_interval=120, timeout=2 * 3600) + yesterday = PartitionPublishedSensor(task_id="wait_yesterday", table="orders", + partition="{{ macros.ds_add(ds, -1) }}", poke_interval=120, timeout=2 * 3600) + + @task + def diff(today_files: list[str], yesterday_files: list[str]): + added = sorted(set(today_files) - set(yesterday_files)) + print(f"{len(added)} new files") + return added + + diff(today.output, yesterday.output) + + +orders_diff() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/requirements.txt b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/requirements.txt new file mode 100644 index 0000000..63ce0cc --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +requests==2.34.2 +httpx==0.28.1 diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/tools/catalog_stub.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/tools/catalog_stub.py new file mode 100644 index 0000000..768f04d --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/fixture/tools/catalog_stub.py @@ -0,0 +1,104 @@ +"""Local stand-in for the lake catalog API. + + python tools/catalog_stub.py --publish orders/2026-09-29=0 --publish orders/2026-09-28=5 + +Flags: + --publish TABLE/PARTITION=SECONDS partition becomes PUBLISHED that many seconds after startup + (repeatable; any other partition stays PENDING) + --latency S delay added to every request, like the real API under load + --down every request answers HTTP 503 + --outage START END every request answers HTTP 503 between START and END seconds + after the first request + --port N default: a free port, printed at startup + --log FILE JSONL record of requests +Token: eval-lake-token. +""" + +from __future__ import annotations + +import argparse +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +TOKEN = "eval-lake-token" + + +def files_for(table: str, partition: str) -> list[str]: + return [f"s3://lake/{table}/{partition}/part-{i}.parquet" for i in range(2)] + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=0) + ap.add_argument("--port-file", default=None) + ap.add_argument("--log", default=None) + ap.add_argument("--latency", type=float, default=0.0) + ap.add_argument("--down", action="store_true") + ap.add_argument("--outage", type=float, nargs=2, default=None) + ap.add_argument("--publish", action="append", default=[]) + args = ap.parse_args() + started = time.time() + first = {"t": None} + publish = {} + for spec in args.publish: + key, _, delay = spec.partition("=") + publish[key] = float(delay or 0) + lock = threading.Lock() + + def log(**rec): + if args.log: + rec["t"] = time.time() + with lock, open(args.log, "a") as fh: + fh.write(json.dumps(rec) + "\n") + + class H(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *a): + pass + + def _send(self, code, body): + data = json.dumps(body).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + if args.latency: + time.sleep(args.latency) + parts = [p for p in self.path.split("?")[0].split("/") if p] + if self.headers.get("Authorization") != f"Bearer {TOKEN}": + log(event="unauthorized", path=self.path) + return self._send(401, {"error": "unauthorized"}) + with lock: + if first["t"] is None: + first["t"] = time.time() + in_outage = bool(args.outage) and args.outage[0] <= time.time() - first["t"] < args.outage[1] + if args.down or in_outage: + log(event="503", path=self.path) + return self._send(503, {"error": "service unavailable"}) + if len(parts) == 5 and parts[:2] == ["v1", "tables"] and parts[3] == "partitions": + table, partition = parts[2], parts[4] + delay = publish.get(f"{table}/{partition}") + ready = delay is not None and time.time() - started >= delay + log(event="status", table=table, partition=partition, ready=ready) + if ready: + return self._send(200, {"state": "PUBLISHED", "files": files_for(table, partition)}) + return self._send(200, {"state": "PENDING"}) + self._send(404, {"error": "not found"}) + + srv = ThreadingHTTPServer(("127.0.0.1", args.port), H) + srv.daemon_threads = True + if args.port_file: + with open(args.port_file, "w") as fh: + fh.write(str(srv.server_address[1])) + print(f"lake catalog listening on http://127.0.0.1:{srv.server_address[1]} (token {TOKEN})", flush=True) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grade.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grade.py new file mode 100644 index 0000000..2500287 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grade.py @@ -0,0 +1,212 @@ +"""Grader for hard-deferrable-debug-partition-sensor (Airflow 3.3). + +The agent fixes the root causes behind seven production incidents in a deferrable sensor. +Grading drives the agent's PartitionPublishedSensor through hidden scenarios with grader-only +DAGs (grader_data/eval_dags/) against a grader-owned catalog API (grader_data/lake_api.py, +every request takes 0.5 s), using `airflow dags test` wrapped by grader_data/defer_runner.py +(deferral timeouts enforced with a 4 s grace, triggerer restart mid-wait, event-loop stall +measurement; see that file). + +Scenarios (each with its own API server and metadata DB, run in parallel): + wait partition published 4 s after start; sensor must defer, return its files, never stall the loop + timeout never published, sensor timeout=10 s, retries=2, triggerer restarted 8 s into the wait + softfail never published, timeout=6 s, soft_fail=True + outage every request answers 503 + two_days sensors for D and D-1 of the same table in one run + budget 6 s upstream task, timeout=16 s, retries=4, 503s 3-8 s after the first request +""" + +from __future__ import annotations + +import concurrent.futures as cf +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +CASE_DIR = Path(os.path.abspath(__file__)).parent # no resolve(): a symlinked case dir stays as given +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +GD = CASE_DIR / "grader_data" +TOKEN = "eval-lake-token" +LATENCY_S = 0.5 +STALL_FAIL_S = 0.35 +DAY, PREV = "2026-09-29", "2026-09-28" +TIMEOUT_S = 10.0 +FAIL_WINDOW = (TIMEOUT_S - 1.5, TIMEOUT_S + 7.0) # trigger deadline, or defer timeout + 4 s grace +RUN_TIMEOUT_S = 400 +BUDGET_S = 16.0 # budget scenario: 6 s upstream task, sensor timeout=16 s, retries=4, 503s 3-8 s in + +SCENARIOS = { + "wait": dict(server=["--publish", f"orders/{DAY}=4"], dag="eval_lake_wait"), + "timeout": dict(server=[], dag="eval_lake_timeout", env={"EVAL_RESTART_AFTER_S": "8"}), + "softfail": dict(server=[], dag="eval_lake_softfail"), + "outage": dict(server=["--down"], dag="eval_lake_outage"), + "budget": dict(server=["--outage", "3", "8"], dag="eval_lake_budget"), + "two_days": dict(server=["--publish", f"orders/{DAY}=0", "--publish", f"orders/{PREV}=0"], + dag="eval_lake_two_days"), +} + +TI_PROBE = r''' +import sqlite3, os +db = os.environ["AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"].split("sqlite:///", 1)[1] +con = sqlite3.connect(db) +rows = con.execute("SELECT dag_id, task_id, map_index, state, try_number FROM task_instance").fetchall() +RESULT = [dict(zip(["dag_id", "task_id", "map_index", "state", "try_number"], r)) for r in rows] +''' + + +def files_for(table: str, partition: str) -> list[str]: + return [f"s3://lake/{table}/{partition}/part-{i}.parquet" for i in range(2)] + + +def read_jsonl(path: Path) -> list[dict]: + out = [] + if path.exists(): + for line in path.read_text().splitlines(): + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + pass + return out + + +def run_scenario(py: str, ws: Path, name: str, spec: dict) -> dict: + tmp = Path(tempfile.mkdtemp(prefix=f"eval-lake-{name}-")) + port_file, api_log, defer_log, out_dir = tmp / "port", tmp / "api.jsonl", tmp / "defer.jsonl", tmp / "out" + out_dir.mkdir() + server = subprocess.Popen( + [py, str(GD / "lake_api.py"), "--port-file", str(port_file), "--log", str(api_log), + "--latency", str(LATENCY_S), *spec["server"]], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) + try: + for _ in range(100): + if port_file.exists() and port_file.read_text().strip(): + break + time.sleep(0.1) + port = int(port_file.read_text().strip()) + env = g.airflow_env(ws, env_py=py, extra={ + "AIRFLOW_CONN_LAKE_DEFAULT": json.dumps( + {"conn_type": "http", "host": "127.0.0.1", "port": port, "password": TOKEN}), + "EVAL_OUT_DIR": str(out_dir), + "EVAL_DEFER_LOG": str(defer_log), + **spec.get("env", {}), + }) + db = g.ensure_db(py, env) + if not db.ok: + return {"name": name, "error": "db migrate failed: " + db.tail(20)} + res = g.run_cmd([py, str(GD / "defer_runner.py"), "dags", "test", spec["dag"], DAY], + env=env, cwd=ws, timeout=RUN_TIMEOUT_S) + tis, _ = g.probe_json(py, TI_PROBE, ws, env=env) + outputs = {p.stem: json.loads(p.read_text()) for p in out_dir.glob("*.json")} + return {"name": name, "api": read_jsonl(api_log), "defer": read_jsonl(defer_log), "tis": tis or [], + "outputs": outputs, "log": res.output[-8000:] + ("\n[timed out]" if res.timed_out else "")} + finally: + server.kill() + server.wait() + + +def ti(r: dict, task_id: str) -> dict: + return next((t for t in r.get("tis", []) if t["task_id"] == task_id), {}) + + +def brief(r: dict) -> str: + trig = [{k: v for k, v in e.items() if k in ("kind", "task_id", "try_number", "waited_s", "payload", "timeout_s", + "state", "stalls")} + for e in r.get("defer", [])] + return json.dumps({"tis": [(t["task_id"], t["state"], t["try_number"]) for t in r.get("tis", [])], + "outputs": r.get("outputs"), "events": trig[:14]}, default=str)[:2500] + + +def log_tail(r: dict, n: int = 2000) -> str: + keep = [ln for ln in r.get("log", "").splitlines() + if any(k in ln for k in ("Error", "Exception", "Traceback", "raise ", "File \"", "timed out"))] + return "\n".join(keep)[-n:] + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + penv = g.airflow_env(ws, env_py=py) + penv["PYTHONPATH"] = os.pathsep.join((str(ws / "dags"), penv["PYTHONPATH"])) + imp = g.import_dags(py, ws, env=penv) + errs = "; ".join(f"{k}: {v.strip().splitlines()[-1]}" for k, v in imp["import_errors"].items()) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"] + and {"lake_ingest", "orders_diff"} <= set(imp["dags"]), + imp["probe_error"] or errs or f"dags: {sorted(imp['dags'])}") + + run_ws = g.copy_workspace(ws) + shutil.copy2(GD / "eval_dags" / "_eval_lake_scenarios.py", run_ws / "dags" / "_eval_lake_scenarios.py") + with cf.ThreadPoolExecutor(max_workers=len(SCENARIOS)) as ex: + futs = {n: ex.submit(run_scenario, py, run_ws, n, s) for n, s in SCENARIOS.items()} + R = {n: f.result() for n, f in futs.items()} + + w = R["wait"] + deferred = any(e["kind"] == "defer" for e in w.get("defer", [])) + ok = ti(w, "wait").get("state") == "success" and w["outputs"].get(f"wait_{DAY}") == files_for("orders", DAY) + grader.primary("sensor still defers and returns the published files", ok and deferred, + f"deferred={deferred}\n" + brief(w) + ("" if ok else "\n" + log_tail(w))) + + stalls = sorted((s for r in R.values() for e in r.get("defer", []) if e["kind"].startswith("trigger_") + for s in e.get("stalls", [])), reverse=True) + big = [s for s in stalls if s >= STALL_FAIL_S] + ran = sum(1 for r in R.values() for e in r.get("defer", []) if e["kind"].startswith("trigger_")) + grader.primary("trigger never blocks the event loop (catalog API answers in 0.5 s)", ran > 0 and len(big) < 2, + f"{ran} trigger runs; stalls >= {STALL_FAIL_S}s: {big[:10]}") + + t = R["timeout"] + t_ti = ti(t, "wait") + defers = [e for e in t.get("defer", []) if e["kind"] == "defer" and e["task_id"] == "wait"] + ends = [e for e in t.get("defer", []) if e["kind"] == "task_end" and e["task_id"] == "wait"] + restarted = any(e["kind"] == "restart" for e in t.get("defer", [])) + dt = round(ends[0]["t"] - defers[0]["t"], 2) if defers and ends else None + ok = (t_ti.get("state") == "failed" and t_ti.get("try_number") == 1) + grader.primary("a timed-out sensor fails for good (no retries)", ok, + brief(t) + ("" if ok else "\n" + log_tail(t))) + in_window = dt is not None and FAIL_WINDOW[0] <= dt <= FAIL_WINDOW[1] + grader.primary("triggerer restart does not extend the sensor timeout", restarted and in_window + and t_ti.get("state") == "failed", + f"restart happened={restarted}; first try ended {dt}s after deferring " + f"(timeout={TIMEOUT_S}s, want {FAIL_WINDOW[0]}-{FAIL_WINDOW[1]}s)\n" + brief(t)) + + s = R["softfail"] + ok = ti(s, "wait").get("state") == "skipped" + grader.primary("soft_fail sensor is skipped when it times out", ok, brief(s) + ("" if ok else "\n" + log_tail(s))) + + o = R["outage"] + ok = ti(o, "wait").get("state") == "failed" and ti(o, "collect").get("state") != "success" \ + and not o["outputs"] + grader.primary("API errors fail the sensor instead of letting the load run", ok, + brief(o) + ("" if ok else "\n" + log_tail(o))) + + bu = R["budget"] + starts = [e for e in bu.get("defer", []) if e["kind"] == "worker_run" and e["task_id"] == "wait"] + ends = [e for e in bu.get("defer", []) if e["kind"] == "task_end" and e["task_id"] == "wait"] + span = round(ends[-1]["t"] - starts[0]["t"], 2) if starts and ends else None + tries = ti(bu, "wait").get("try_number", 0) + lo, hi = BUDGET_S - 1.5, BUDGET_S + 5.5 + ok = ti(bu, "wait").get("state") == "failed" and tries >= 2 and span is not None and lo <= span <= hi + grader.primary("timeout is one budget per DAG run: a retry after an outage does not restart it", ok, + f"sensor ran from first try start to final failure in {span}s over {tries} tries " + f"(timeout={BUDGET_S}s, want {lo}-{hi}s)\n" + brief(bu) + ("" if ok else "\n" + log_tail(bu))) + + d = R["two_days"] + want = {f"today_{DAY}": files_for("orders", DAY), f"yesterday_{DAY}": files_for("orders", PREV)} + ok = all(d["outputs"].get(k) == v for k, v in want.items()) + grader.primary("sensors for two partitions of one table each get their own files", ok, + f"want={want}\n" + brief(d) + ("" if ok else "\n" + log_tail(d))) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/defer_runner.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/defer_runner.py new file mode 100644 index 0000000..43f9fae --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/defer_runner.py @@ -0,0 +1,290 @@ +"""`airflow dags test` with the production triggerer/scheduler behaviours it skips (Airflow 3.3.2). + +Usage: python defer_runner.py dags test + +Plain `dags test` runs each trigger inline until its first event. That matches production +for the happy path (the trigger is rebuilt from ``serialize()``'s classpath + kwargs and the +task resumes in a fresh operator instance), but skips things a real deployment does. This +wrapper replaces ``airflow.sdk.definitions.dag._run_task`` with a copy that adds them, each +modelled on airflow/jobs/triggerer_job_runner.py, models/trigger.py and the scheduler: + +* **Deferral timeouts.** ``defer(timeout=...)`` is enforced like the scheduler's + ``check_trigger_timeouts``: once it passes (plus ``EVAL_DEFER_TIMEOUT_GRACE_S``, standing in + for the scheduler's 15 s check interval) the trigger is cancelled (no ``on_kill``) and the + task resumes with ``next_method="__fail__"``, ``error=TRIGGER_TIMEOUT``. +* **Triggerer restarts** (``EVAL_RESTART_AFTER_S``): after that many seconds the running trigger + is cancelled without a message (the shutdown path: no ``on_kill``), ``cleanup()`` is awaited, + and a new instance is built from the classpath + kwargs stored at deferral time. +* **User action** (``EVAL_USER_KILL_AFTER_S``): after that many seconds the task instance is + marked failed in the metadata DB (what "mark failed" in the UI does), the trigger task is + cancelled with the triggerer's user-action message, ``on_kill()`` is awaited (30 s bound), + then ``cleanup()``. The task does not resume. +* **A trigger that raises or ends without an event** fails the task with + ``error=TRIGGER_FAILURE``, as the triggerer does. +* **Task-end events.** A ``TaskSuccessEvent``/``TaskFailedEvent``/``TaskSkippedEvent`` from the + trigger is applied with the production handler (``models.trigger.handle_event_submit``): + the task instance gets its final state and XComs without resuming on a worker. +* **Event + defer kwargs.** The resume method gets ``defer(kwargs=...)`` merged with + ``event=`` as in production; plain ``dags test`` drops the defer kwargs. +* The trigger's ``task_instance`` is set (dag_id, task_id, run_id, map_index, try_number) and + the task instance stays ``deferred`` in the DB while the trigger runs, as in production. + +It also measures event-loop stalls while triggers run (a 20 ms ticker; gaps over +``STALL_RECORD_S`` are recorded) and writes JSONL records to ``EVAL_DEFER_LOG``. +A trigger still running after ``EVAL_TRIGGER_HARD_CAP_S`` is cancelled and recorded as a hang. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import json +import os +import sys +import time +import traceback +from uuid import UUID + +LOG_PATH = os.environ.get("EVAL_DEFER_LOG") +RESTART_AFTER_S = float(os.environ.get("EVAL_RESTART_AFTER_S") or 0) +USER_KILL_AFTER_S = float(os.environ.get("EVAL_USER_KILL_AFTER_S") or 0) +GRACE_S = float(os.environ.get("EVAL_DEFER_TIMEOUT_GRACE_S") or 4) +HARD_CAP_S = float(os.environ.get("EVAL_TRIGGER_HARD_CAP_S") or 120) +ON_KILL_TIMEOUT_S = 30 +USER_ACTION_CANCEL_MSG = "__airflow_user_action__" # triggerer_job_runner._USER_ACTION_CANCEL_MSG +STALL_RECORD_S = 0.15 +TICK_S = 0.02 + + +def record(**rec) -> None: + rec["t"] = time.time() + if LOG_PATH: + with open(LOG_PATH, "a") as fh: + fh.write(json.dumps(rec, default=str) + "\n") + + +def _drive_trigger(msg, task_sdk_ti, label: dict, mark_failed): + """Run the deferred trigger like a triggerer would. + + Returns (next_method, next_kwargs), or None when a user action ended the task. + """ + import psutil + import structlog + + from airflow.sdk._shared.module_loading import import_string + from airflow.sdk.bases.operator import TRIGGER_FAIL_REPR, TriggerFailureReason + from airflow.sdk.execution_time.supervisor import ( + InProcessSupervisorComms, + InProcessTestSupervisor, + set_supervisor_comms, + ) + from airflow.sdk.serde import deserialize, serialize + + timeout_s = msg.trigger_timeout.total_seconds() if msg.trigger_timeout is not None else None + deferred_at = time.time() + fail_at = deferred_at + timeout_s + GRACE_S if timeout_s is not None else None + + def build(): + trig = import_string(msg.classpath)(**deserialize(msg.trigger_kwargs)) + with contextlib.suppress(Exception): + trig.task_instance = task_sdk_ti + return trig + + async def first_event(trig): + return await anext(trig.run(), None) + + async def stop(task, trig, message=None): + task.cancel(message) if message else task.cancel() + with contextlib.suppress(BaseException): + await task + + async def main(): + loop = asyncio.get_running_loop() + stalls: list[float] = [] + + async def monitor(): + last = loop.time() + while True: + await asyncio.sleep(TICK_S) + now = loop.time() + gap = now - last - TICK_S + if gap > STALL_RECORD_S: + stalls.append(round(gap, 3)) + last = now + + mon = asyncio.create_task(monitor()) + trig = build() + task = asyncio.create_task(first_event(trig)) + outcome: tuple[str, object] = ("hang", None) + try: + if RESTART_AFTER_S: + done, _ = await asyncio.wait({task}, timeout=RESTART_AFTER_S) + if task not in done: + await stop(task, trig) + with contextlib.suppress(Exception): + await trig.cleanup() + record(kind="restart", **label) + trig = build() + task = asyncio.create_task(first_event(trig)) + limit = HARD_CAP_S + if fail_at is not None: + limit = min(limit, max(0.0, fail_at - time.time())) + user_kill = bool(USER_KILL_AFTER_S) and USER_KILL_AFTER_S < limit + done, _ = await asyncio.wait({task}, timeout=USER_KILL_AFTER_S if user_kill else limit) + if task in done: + try: + outcome = ("event", task.result()) + except BaseException as exc: # noqa: BLE001 - mirrors the triggerer + outcome = ("error", "".join(traceback.format_exception(exc))) + elif user_kill: + mark_failed() + record(kind="user_kill", **label) + await stop(task, trig, USER_ACTION_CANCEL_MSG) + try: + await asyncio.wait_for(trig.on_kill(), timeout=ON_KILL_TIMEOUT_S) + except Exception as exc: # noqa: BLE001 - the triggerer only logs these + record(kind="on_kill_error", error=repr(exc), **label) + outcome = ("user_kill", None) + else: + await stop(task, trig) + outcome = ("timeout", None) if fail_at is not None and time.time() >= fail_at - 0.05 else ("hang", None) + finally: + with contextlib.suppress(Exception): + await trig.cleanup() + mon.cancel() + return outcome, stalls + + supervisor = InProcessTestSupervisor( + id=task_sdk_ti.id, + pid=os.getpid(), + process=psutil.Process(), + process_log=structlog.get_logger(logger_name="task").bind(), + client=InProcessTestSupervisor._api_client(), + ) + supervisor.comms = InProcessSupervisorComms(supervisor=supervisor) + with set_supervisor_comms(supervisor.comms): + (kind, value), stalls = asyncio.run(main()) + + rec = dict(kind="trigger_" + kind, waited_s=round(time.time() - deferred_at, 3), stalls=stalls, **label) + if kind == "user_kill": + record(**rec) + return None + if kind == "event": + if value is None: + rec["kind"] = "trigger_no_event" + record(**rec) + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, + "traceback": ["trigger exited without yielding an event"]} + rec["payload"] = value.payload + rec["event_type"] = type(value).__name__ + record(**rec) + from airflow.triggers.base import BaseTaskEndEvent + + if isinstance(value, BaseTaskEndEvent): + return ("end_event", value) + # Production (models/trigger.py handle_event_submit) merges the event into the defer + # kwargs; plain `dags test` 3.3.2 replaces them with {"event": ...}. + next_kwargs = dict(deserialize(msg.next_kwargs) or {}) if msg.next_kwargs else {} + next_kwargs["event"] = value.payload + return msg.next_method, serialize(next_kwargs) + if kind == "error": + rec["traceback"] = value + record(**rec) + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, "traceback": str(value).splitlines()} + record(**rec) + if kind == "timeout": + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_TIMEOUT} + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, + "traceback": [f"eval: trigger did not fire within {HARD_CAP_S}s"]} + + +def _run_task(*, ti, task, run_triggerer: bool = False): + """Copy of airflow.sdk.definitions.dag._run_task (3.3.2) with the inline trigger replaced.""" + import airflow.sdk.definitions.dag as dagmod + from airflow.sdk.api.datamodels._generated import TaskInstance as TaskInstanceSDK + from airflow.sdk.execution_time.comms import DeferTask + from airflow.sdk.execution_time.supervisor import run_task_in_process + from airflow.serialization.serialized_objects import create_scheduler_operator + from airflow.utils.session import create_session + from airflow.utils.state import TaskInstanceState + + log = dagmod.log + taskrun_result = None + log.info("[DAG TEST] starting task_id=%s map_index=%s", ti.task_id, ti.map_index) + while True: + try: + ti.set_state(TaskInstanceState.QUEUED) + task_sdk_ti = TaskInstanceSDK( + id=UUID(str(ti.id)), + task_id=ti.task_id, + dag_id=ti.dag_id, + run_id=ti.run_id, + try_number=ti.try_number, + map_index=ti.map_index, + dag_version_id=UUID(str(ti.dag_version_id)), + ) + record(kind="worker_run", task_id=ti.task_id, map_index=ti.map_index, try_number=ti.try_number, + run_id=ti.run_id) + taskrun_result = run_task_in_process(ti=task_sdk_ti, task=task) + msg = taskrun_result.msg + ti.set_state(taskrun_result.ti.state) + ti.task = create_scheduler_operator(taskrun_result.ti.task) + label = {"task_id": ti.task_id, "map_index": ti.map_index, "try_number": ti.try_number, + "run_id": ti.run_id} + if ti.state == TaskInstanceState.DEFERRED and isinstance(msg, DeferTask) and run_triggerer: + record(kind="defer", classpath=msg.classpath, trigger_kwargs=msg.trigger_kwargs, + timeout_s=msg.trigger_timeout.total_seconds() if msg.trigger_timeout else None, + next_method=msg.next_method, next_kwargs=msg.next_kwargs, **label) + resumed = _drive_trigger(msg, task_sdk_ti, label, + mark_failed=lambda: ti.set_state(TaskInstanceState.FAILED)) + if resumed is None: + ti.set_state(TaskInstanceState.FAILED) + record(kind="task_end", state=str(ti.state), **label) + break + if resumed[0] == "end_event": + # The trigger ended the task itself (TaskSuccessEvent & co): apply it with the + # production handler (models/trigger.py), no resume on a worker. + from airflow.models.taskinstance import TaskInstance as TIModel + from airflow.models.trigger import handle_event_submit + + with create_session() as session: + db_ti = session.get(TIModel, ti.id) + handle_event_submit(resumed[1], task_instance=db_ti, session=session) + final_state = db_ti.state + ti.refresh_from_db() + record(kind="task_end", state=str(final_state), via="trigger_end_event", **label) + if final_state == TaskInstanceState.UP_FOR_RETRY: + taskrun_result = None + break + ti.set_state(TaskInstanceState.QUEUED) + ti.next_method, ti.next_kwargs = resumed + with create_session() as session: + ti.state = TaskInstanceState.SCHEDULED + session.add(ti) + continue + record(kind="task_end", state=str(ti.state), **label) + break + except Exception: + log.exception("[DAG TEST] Error running task %s", ti) + record(kind="harness_exception", traceback=traceback.format_exc()) + if ti.state not in dagmod.FINISHED_STATES: + ti.set_state(TaskInstanceState.FAILED) + taskrun_result = None + break + raise + log.info("[DAG TEST] end task task_id=%s map_index=%s", ti.task_id, ti.map_index) + return taskrun_result + + +def main() -> None: + import airflow.sdk.definitions.dag as dagmod + + dagmod._run_task = _run_task + from airflow.__main__ import main as airflow_main + + sys.argv = ["airflow", *sys.argv[1:]] + airflow_main() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/eval_dags/_eval_lake_scenarios.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/eval_dags/_eval_lake_scenarios.py new file mode 100644 index 0000000..5a629bc --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/eval_dags/_eval_lake_scenarios.py @@ -0,0 +1,77 @@ +"""Grader-only DAGs that drive the agent's PartitionPublishedSensor through hidden scenarios.""" + +from __future__ import annotations + +import json +import os +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +from lake.sensors import PartitionPublishedSensor + + +@task +def collect(name: str, files, ds=None): + (Path(os.environ["EVAL_OUT_DIR"]) / f"{name}_{ds}.json").write_text(json.dumps(files)) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, default_args={"retries": 0}) +def eval_lake_wait(): + s = PartitionPublishedSensor(task_id="wait", table="orders", poke_interval=1, timeout=300) + collect("wait", s.output) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(seconds=3)}) +def eval_lake_timeout(): + s = PartitionPublishedSensor(task_id="wait", table="orders", poke_interval=1, timeout=10) + collect("timeout", s.output) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, + default_args={"retries": 2, "retry_delay": timedelta(seconds=3)}) +def eval_lake_softfail(): + s = PartitionPublishedSensor(task_id="wait", table="customers", poke_interval=1, timeout=6, soft_fail=True) + collect("softfail", s.output) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, + default_args={"retries": 1, "retry_delay": timedelta(seconds=2)}) +def eval_lake_outage(): + s = PartitionPublishedSensor(task_id="wait", table="orders", poke_interval=1, timeout=60) + collect("outage", s.output) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, default_args={"retries": 0}) +def eval_lake_two_days(): + today = PartitionPublishedSensor(task_id="wait_today", table="orders", partition="{{ ds }}", + poke_interval=1, timeout=300) + yesterday = PartitionPublishedSensor(task_id="wait_yesterday", table="orders", + partition="{{ macros.ds_add(ds, -1) }}", poke_interval=1, timeout=300) + today >> yesterday + collect.override(task_id="collect_today")("today", today.output) + collect.override(task_id="collect_yesterday")("yesterday", yesterday.output) + + +@dag(schedule=None, start_date=datetime(2026, 9, 1), catchup=False, + default_args={"retries": 4, "retry_delay": timedelta(seconds=2)}) +def eval_lake_budget(): + @task + def staging(): + import time + + time.sleep(6) + + s = PartitionPublishedSensor(task_id="wait", table="orders", poke_interval=1, timeout=16) + staging() >> s + collect("budget", s.output) + + +eval_lake_budget() +eval_lake_wait() +eval_lake_timeout() +eval_lake_softfail() +eval_lake_outage() +eval_lake_two_days() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/lake_api.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/lake_api.py new file mode 100644 index 0000000..768f04d --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/grader_data/lake_api.py @@ -0,0 +1,104 @@ +"""Local stand-in for the lake catalog API. + + python tools/catalog_stub.py --publish orders/2026-09-29=0 --publish orders/2026-09-28=5 + +Flags: + --publish TABLE/PARTITION=SECONDS partition becomes PUBLISHED that many seconds after startup + (repeatable; any other partition stays PENDING) + --latency S delay added to every request, like the real API under load + --down every request answers HTTP 503 + --outage START END every request answers HTTP 503 between START and END seconds + after the first request + --port N default: a free port, printed at startup + --log FILE JSONL record of requests +Token: eval-lake-token. +""" + +from __future__ import annotations + +import argparse +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +TOKEN = "eval-lake-token" + + +def files_for(table: str, partition: str) -> list[str]: + return [f"s3://lake/{table}/{partition}/part-{i}.parquet" for i in range(2)] + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=0) + ap.add_argument("--port-file", default=None) + ap.add_argument("--log", default=None) + ap.add_argument("--latency", type=float, default=0.0) + ap.add_argument("--down", action="store_true") + ap.add_argument("--outage", type=float, nargs=2, default=None) + ap.add_argument("--publish", action="append", default=[]) + args = ap.parse_args() + started = time.time() + first = {"t": None} + publish = {} + for spec in args.publish: + key, _, delay = spec.partition("=") + publish[key] = float(delay or 0) + lock = threading.Lock() + + def log(**rec): + if args.log: + rec["t"] = time.time() + with lock, open(args.log, "a") as fh: + fh.write(json.dumps(rec) + "\n") + + class H(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *a): + pass + + def _send(self, code, body): + data = json.dumps(body).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + if args.latency: + time.sleep(args.latency) + parts = [p for p in self.path.split("?")[0].split("/") if p] + if self.headers.get("Authorization") != f"Bearer {TOKEN}": + log(event="unauthorized", path=self.path) + return self._send(401, {"error": "unauthorized"}) + with lock: + if first["t"] is None: + first["t"] = time.time() + in_outage = bool(args.outage) and args.outage[0] <= time.time() - first["t"] < args.outage[1] + if args.down or in_outage: + log(event="503", path=self.path) + return self._send(503, {"error": "service unavailable"}) + if len(parts) == 5 and parts[:2] == ["v1", "tables"] and parts[3] == "partitions": + table, partition = parts[2], parts[4] + delay = publish.get(f"{table}/{partition}") + ready = delay is not None and time.time() - started >= delay + log(event="status", table=table, partition=partition, ready=ready) + if ready: + return self._send(200, {"state": "PUBLISHED", "files": files_for(table, partition)}) + return self._send(200, {"state": "PENDING"}) + self._send(404, {"error": "not found"}) + + srv = ThreadingHTTPServer(("127.0.0.1", args.port), H) + srv.daemon_threads = True + if args.port_file: + with open(args.port_file, "w") as fh: + fh.write(str(srv.server_address[1])) + print(f"lake catalog listening on http://127.0.0.1:{srv.server_address[1]} (token {TOKEN})", flush=True) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/api_errors_polled_through/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/api_errors_polled_through/dags/lake/triggers.py new file mode 100644 index 0000000..6982ad3 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/api_errors_polled_through/dags/lake/triggers.py @@ -0,0 +1,65 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.deadline, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError: + # MUTANT: polls through API errors instead of failing the try (incident 4 asks + # for the try to fail so the task's retries take over). + await asyncio.sleep(min(self.poll_interval, remaining)) + continue + if info["state"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/blocking_hook_kept/dags/lake/hooks.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/blocking_hook_kept/dags/lake/hooks.py new file mode 100644 index 0000000..4d1973f --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/blocking_hook_kept/dags/lake/hooks.py @@ -0,0 +1,54 @@ +"""Hook for the data-lake catalog API. + +GET /v1/tables/
/partitions/ + 200 {"state": "PENDING"} or {"state": "PUBLISHED", "files": ["s3://...", ...]} + anything else is an error +""" + +from __future__ import annotations + +import requests +from airflow.sdk import BaseHook + + +class LakeApiError(Exception): + """The catalog API answered with an error or could not be reached.""" + + +class LakeCatalogHook(BaseHook): + """Connection: ``host``/``port`` locate the API, ``schema`` is the URL scheme, ``password`` the token.""" + + conn_name_attr = "lake_conn_id" + default_conn_name = "lake_default" + conn_type = "http" + hook_name = "Lake catalog" + + def __init__(self, lake_conn_id: str = default_conn_name, request_timeout: float = 20.0) -> None: + super().__init__() + self.lake_conn_id = lake_conn_id + self.request_timeout = request_timeout + + @staticmethod + def _url_and_headers(conn, table: str, partition: str) -> tuple[str, dict[str, str]]: + port = f":{conn.port}" if conn.port else "" + url = f"{conn.schema or 'http'}://{conn.host}{port}/v1/tables/{table}/partitions/{partition}" + return url, {"Authorization": f"Bearer {conn.password}"} + + def _fetch(self, url: str, headers: dict[str, str]) -> dict: + try: + resp = requests.get(url, headers=headers, timeout=self.request_timeout) + except requests.RequestException as exc: + raise LakeApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise LakeApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() + + def get_partition(self, table: str, partition: str) -> dict: + url, headers = self._url_and_headers(self.get_connection(self.lake_conn_id), table, partition) + return self._fetch(url, headers) + + async def aget_partition(self, table: str, partition: str) -> dict: + """Async variant for triggers.""" + conn = await self.aget_connection(self.lake_conn_id) + url, headers = self._url_and_headers(conn, table, partition) + return self._fetch(url, headers) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/budget_restarts_each_try/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/budget_restarts_each_try/dags/lake/sensors.py new file mode 100644 index 0000000..fa27c49 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/budget_restarts_each_try/dags/lake/sensors.py @@ -0,0 +1,79 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + # The timeout is a budget per DAG run: remember when the first try started waiting. + # The task state store keeps its rows across retries (XComs are cleared on retry). + started = datetime.now(timezone.utc).isoformat() + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + msg = f"{self.table}/{self.partition} was not published within {self.timeout}s" + raise AirflowSkipException(msg) if self.soft_fail else AirflowSensorTimeout(msg) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + deadline=deadline, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + status = event["status"] + if status == "timeout": + raise AirflowSensorTimeout(f"{self.table}/{self.partition} was not published within {self.timeout}s") + if status != "published": + raise AirflowException(f"catalog API error for {self.table}/{self.partition}: {event.get('message')}") + files = event["files"] + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/cache_keyed_by_table/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/cache_keyed_by_table/dags/lake/triggers.py new file mode 100644 index 0000000..f532247 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/cache_keyed_by_table/dags/lake/triggers.py @@ -0,0 +1,70 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +_PUBLISHED: dict[str, dict] = {} + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.deadline, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + if self.table in _PUBLISHED: + yield TriggerEvent({"status": "published", "files": _PUBLISHED[self.table]["files"]}) + return + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + _PUBLISHED[self.table] = info + yield TriggerEvent({"status": "published", "files": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/errors_as_success/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/errors_as_success/dags/lake/sensors.py new file mode 100644 index 0000000..6dd6965 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/errors_as_success/dags/lake/sensors.py @@ -0,0 +1,81 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + # The timeout is a budget per DAG run: remember when the first try started waiting. + # The task state store keeps its rows across retries (XComs are cleared on retry). + store = context["task_state_store"] + started = store.get("partition_wait_started_at") + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set("partition_wait_started_at", started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + msg = f"{self.table}/{self.partition} was not published within {self.timeout}s" + raise AirflowSkipException(msg) if self.soft_fail else AirflowSensorTimeout(msg) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + deadline=deadline, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + status = event["status"] + if status == "timeout": + raise AirflowSensorTimeout(f"{self.table}/{self.partition} was not published within {self.timeout}s") + files = event.get("files", []) + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/finishes_from_triggerer/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/finishes_from_triggerer/dags/lake/triggers.py new file mode 100644 index 0000000..2adb38e --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/finishes_from_triggerer/dags/lake/triggers.py @@ -0,0 +1,63 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TaskSuccessEvent, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.deadline, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + yield TaskSuccessEvent(xcoms={"return_value": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/hardcoded_partition_files/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/hardcoded_partition_files/dags/lake/triggers.py new file mode 100644 index 0000000..6163fd0 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/hardcoded_partition_files/dags/lake/triggers.py @@ -0,0 +1,68 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.deadline, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + # MUTANT (adversarial): never asks the catalog, fabricates the expected file list. + await asyncio.sleep(self.poll_interval) + yield TriggerEvent({"status": "published", + "files": [f"s3://lake/{self.table}/{self.partition}/part-{i}.parquet" for i in range(2)]}) + return + hook = LakeCatalogHook(self.lake_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/sensors.py new file mode 100644 index 0000000..7c7cb5d --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/sensors.py @@ -0,0 +1,82 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + # The timeout is a budget per DAG run: remember when the first try started waiting. + # The task state store keeps its rows across retries (XComs are cleared on retry). + store = context["task_state_store"] + started = store.get("partition_wait_started_at") + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set("partition_wait_started_at", started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + msg = f"{self.table}/{self.partition} was not published within {self.timeout}s" + raise AirflowSkipException(msg) if self.soft_fail else AirflowSensorTimeout(msg) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + deadline=deadline, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + status = event["status"] + if status == "timeout": + raise AirflowSensorTimeout(f"{self.table}/{self.partition} was not published within {self.timeout}s") + if status != "published": + raise AirflowException(f"catalog API error for {self.table}/{self.partition}: {event.get('message')}") + files = event["files"] + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/triggers.py new file mode 100644 index 0000000..db05015 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/relative_deadline_no_defer_timeout/dags/lake/triggers.py @@ -0,0 +1,66 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +import time +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.timeout_s = deadline if isinstance(deadline, (int, float)) else (deadline - datetime.now(timezone.utc)).total_seconds() + self._give_up_at = time.monotonic() + self.timeout_s + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.timeout_s, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + remaining = self._give_up_at - time.monotonic() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/timeout_retryable/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/timeout_retryable/dags/lake/sensors.py new file mode 100644 index 0000000..06471f7 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/mutants/timeout_retryable/dags/lake/sensors.py @@ -0,0 +1,83 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + # The timeout is a budget per DAG run: remember when the first try started waiting. + # The task state store keeps its rows across retries (XComs are cleared on retry). + store = context["task_state_store"] + started = store.get("partition_wait_started_at") + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set("partition_wait_started_at", started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + msg = f"{self.table}/{self.partition} was not published within {self.timeout}s" + raise AirflowSkipException(msg) if self.soft_fail else AirflowSensorTimeout(msg) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + deadline=deadline, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + status = event["status"] + if status == "timeout": + raise AirflowException(f"{self.table}/{self.partition} was not published within {self.timeout}s") + if status != "published": + raise AirflowException(f"catalog API error for {self.table}/{self.partition}: {event.get('message')}") + files = event["files"] + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/hooks.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/hooks.py new file mode 100644 index 0000000..1e84891 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/hooks.py @@ -0,0 +1,62 @@ +"""Hook for the data-lake catalog API. + +GET /v1/tables/
/partitions/ + 200 {"state": "PENDING"} or {"state": "PUBLISHED", "files": ["s3://...", ...]} + anything else is an error +""" + +from __future__ import annotations + +import httpx +import requests +from airflow.sdk import BaseHook + + +class LakeApiError(Exception): + """The catalog API answered with an error or could not be reached.""" + + +class LakeCatalogHook(BaseHook): + """Connection: ``host``/``port`` locate the API, ``schema`` is the URL scheme, ``password`` the token.""" + + conn_name_attr = "lake_conn_id" + default_conn_name = "lake_default" + conn_type = "http" + hook_name = "Lake catalog" + + def __init__(self, lake_conn_id: str = default_conn_name, request_timeout: float = 20.0) -> None: + super().__init__() + self.lake_conn_id = lake_conn_id + self.request_timeout = request_timeout + + @staticmethod + def _url_and_headers(conn, table: str, partition: str) -> tuple[str, dict[str, str]]: + port = f":{conn.port}" if conn.port else "" + url = f"{conn.schema or 'http'}://{conn.host}{port}/v1/tables/{table}/partitions/{partition}" + return url, {"Authorization": f"Bearer {conn.password}"} + + def _fetch(self, url: str, headers: dict[str, str]) -> dict: + try: + resp = requests.get(url, headers=headers, timeout=self.request_timeout) + except requests.RequestException as exc: + raise LakeApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise LakeApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() + + def get_partition(self, table: str, partition: str) -> dict: + url, headers = self._url_and_headers(self.get_connection(self.lake_conn_id), table, partition) + return self._fetch(url, headers) + + async def aget_partition(self, table: str, partition: str) -> dict: + """Async variant for triggers.""" + conn = await self.aget_connection(self.lake_conn_id) + url, headers = self._url_and_headers(conn, table, partition) + try: + async with httpx.AsyncClient(timeout=self.request_timeout) as client: + resp = await client.get(url, headers=headers) + except httpx.HTTPError as exc: + raise LakeApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise LakeApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/sensors.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/sensors.py new file mode 100644 index 0000000..9693d03 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/sensors.py @@ -0,0 +1,83 @@ +"""Sensor that waits for a lake partition, deferring to the triggerer.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from lake.hooks import LakeCatalogHook +from lake.triggers import PartitionPublishedTrigger + + +class PartitionPublishedSensor(BaseSensorOperator): + """Wait until ``table``/``partition`` is PUBLISHED and return its file list. + + :param table: catalog table name + :param partition: partition key (templated) + :param lake_conn_id: connection to the catalog API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("table", "partition") + + def __init__( + self, + *, + table: str, + partition: str = "{{ ds }}", + lake_conn_id: str = LakeCatalogHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.table = table + self.partition = partition + self.lake_conn_id = lake_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + info = LakeCatalogHook(self.lake_conn_id).get_partition(self.table, self.partition) + if info["state"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=info["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + # The timeout is a budget per DAG run: remember when the first try started waiting. + # The task state store keeps its rows across retries (XComs are cleared on retry). + store = context["task_state_store"] + started = store.get("partition_wait_started_at") + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set("partition_wait_started_at", started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + msg = f"{self.table}/{self.partition} was not published within {self.timeout}s" + raise AirflowSkipException(msg) if self.soft_fail else AirflowSensorTimeout(msg) + self.defer( + trigger=PartitionPublishedTrigger( + table=self.table, + partition=self.partition, + deadline=deadline, + lake_conn_id=self.lake_conn_id, + poll_interval=self.poke_interval, + ), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + status = event["status"] + if status == "timeout": + raise AirflowSensorTimeout(f"{self.table}/{self.partition} was not published within {self.timeout}s") + if status != "published": + raise AirflowException(f"catalog API error for {self.table}/{self.partition}: {event.get('message')}") + files = event["files"] + self.log.info("%s/%s published with %d files", self.table, self.partition, len(files)) + return files diff --git a/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/triggers.py b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/triggers.py new file mode 100644 index 0000000..2fd3a04 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-debug-partition-sensor/reference/dags/lake/triggers.py @@ -0,0 +1,63 @@ +"""Trigger that waits for a lake partition to be published.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from lake.hooks import LakeApiError, LakeCatalogHook + + +class PartitionPublishedTrigger(BaseTrigger): + """Poll the catalog until the partition is PUBLISHED, the API fails, or ``deadline`` passes. + + ``deadline`` is an absolute UTC datetime so a trigger rebuilt after a triggerer restart + keeps it. + """ + + def __init__( + self, + table: str, + partition: str, + deadline: datetime, + lake_conn_id: str = LakeCatalogHook.default_conn_name, + poll_interval: float = 60.0, + ) -> None: + super().__init__() + self.table = table + self.partition = partition + self.deadline = deadline + self.lake_conn_id = lake_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + "lake.triggers.PartitionPublishedTrigger", + { + "table": self.table, + "partition": self.partition, + "deadline": self.deadline, + "lake_conn_id": self.lake_conn_id, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + hook = LakeCatalogHook(self.lake_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + info = await hook.aget_partition(self.table, self.partition) + except LakeApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if info["state"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": info["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/alt_reference/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/alt_reference/dags/partners/sensors.py new file mode 100644 index 0000000..3dbff24 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/alt_reference/dags/partners/sensors.py @@ -0,0 +1,85 @@ +"""Sensor that waits for a partner's daily manifest (alt: Variable anchor, scheduler-enforced timeout).""" + +from __future__ import annotations + +import asyncio +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue, Variable +from airflow.sdk.exceptions import AirflowSensorTimeout +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from partners.hooks import ManifestHook + + +class ManifestApiDown(RuntimeError): + """Raised on resume after an API error so the task's retries apply (never a skip).""" + + +class ManifestTrigger(BaseTrigger): + def __init__(self, partner: str, ds: str, conn_id: str, interval: float) -> None: + super().__init__() + self.partner, self.ds, self.conn_id, self.interval = partner, ds, conn_id, interval + + def serialize(self): + return ("partners.sensors.ManifestTrigger", + {"partner": self.partner, "ds": self.ds, "conn_id": self.conn_id, "interval": self.interval}) + + async def run(self): + hook = ManifestHook(self.conn_id) + while True: + try: + m = await asyncio.to_thread(hook.get_manifest, self.partner, self.ds) + except Exception as exc: # noqa: BLE001 + yield TriggerEvent({"ok": False, "error": str(exc)}) + return + if m["status"] == "PUBLISHED": + yield TriggerEvent({"ok": True, "files": m["files"]}) + return + await asyncio.sleep(self.interval) + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list.""" + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__(self, *, partner: str, ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, deferrable: bool = True, + **kwargs: Any) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + ti = context["ti"] + key = f"manifest_wait_start__{ti.dag_id}__{ti.run_id}__{ti.task_id}__{ti.map_index}" + started = Variable.get(key, default=None) + if started is None: + started = datetime.now(timezone.utc).isoformat() + Variable.set(key, started) + left = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) - datetime.now(timezone.utc) + if left <= timedelta(0): + # resume_execution is not involved here, so honour soft_fail ourselves + from airflow.sdk.exceptions import AirflowSkipException + + raise (AirflowSkipException if self.soft_fail else AirflowSensorTimeout)("manifest wait budget used up") + self.defer(trigger=ManifestTrigger(self.partner, self.ds, self.manifest_conn_id, self.poke_interval), + method_name="done", timeout=left) + + def done(self, context, event): + if event["ok"]: + return event["files"] + raise ManifestApiDown(event["error"]) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/case.yaml b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/case.yaml new file mode 100644 index 0000000..9db8146 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/case.yaml @@ -0,0 +1,53 @@ +id: hard-deferrable-sensor-retry-budget +area: authoring +suite: hard +split: dev +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +grade_timeout_s: 1500 +prompt: | + dags/partners/sensors.py has PartnerManifestSensor, a poke-mode sensor that waits + for a partner's daily manifest through ManifestHook (API in README.md). Hundreds of + these sit waiting every morning and hold worker slots. Please make it deferrable on + our Airflow 3.3 deployment (`deferrable: bool = True` by default), keeping the + constructor arguments the DAGs use. What on-call relies on: + + 1. When the manifest is published, the sensor returns its file list (the load + tasks read it from XCom). + 2. `timeout` is the total time we wait for a partner in a DAG run, counted from + when the sensor first started waiting in that run. Nothing may reset or extend + it: not the task's retries (the DAGs give it retries for API outages), not + triggerer restarts during deploys. When the time is up the sensor fails for + good, or is skipped when soft_fail=True. + 3. An API error (HTTP 5xx or a connection error) fails the current try so the + task's retries take over, for soft_fail sensors too: an outage is not a + reason to skip. + 4. The triggerer's event loop is shared with hundreds of other triggers and the + API takes about a second per call, so nothing may block the event loop. + + tools/ has a local stand-in for the API. +description: | + Hard suite, theme deferrable (Airflow 3.3). Convert a poke sensor into a deferrable + sensor + async trigger + async hook method where `timeout` is a per-DAG-run budget + across retries. Version-specific runtime facts: on 3.3 a retry clears the task + instance's XComs (an XCom anchor is lost), each try gets a new ti.start_date, and + `dags test` sets dag_run.start_date to the logical date; the 3.3 task state store + (context["task_state_store"]) keeps rows across retries (reference), a Variable + keyed by run/task also works. With soft_fail=True, BaseSensorOperator.resume_execution + turns any AirflowException into a skip, so API errors must not be AirflowExceptions. + Graded with grader-only DAGs against a grader-owned manifest API (0.5 s per request) + via grader_data/defer_runner.py (deferral timeouts enforced with a 4 s grace, + triggerer restarted mid-wait, event-loop stalls measured). Primary checks: DAGs + import; sensor defers and returns the file list; no event-loop stall >= 0.35 s; + budget scenario (6 s upstream task, timeout=16 s, retries=4, 503s from 3-8 s after + the first request) fails 14.5-21.5 s after the sensor's first start, after >= 2 + tries; timed-out sensor fails on try 1 with retries=2; a triggerer restart 8 s into + a 12 s timeout still ends 10.5-17.5 s after start; a soft_fail sensor survives a + 503 outage through retries and returns the files; a soft_fail sensor is skipped + when the budget runs out. + Mutants: anchor in XCom, anchor = this try's start, anchor = dag_run.start_date, + API error raised as AirflowException, sync hook call inside the trigger, + deadline relative to trigger construction without defer timeout, Variable anchor + stored as a float, a trigger that fabricates the file list without calling the API, + and a deferrable flag that is accepted but ignored (still pokes). diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/.gitignore b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/.gitignore new file mode 100644 index 0000000..5b478e6 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.log diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/README.md b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/README.md new file mode 100644 index 0000000..5e1ebce --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/README.md @@ -0,0 +1,23 @@ +# partner-pipelines + +Airflow 3.3 DAGs that ingest our partners' daily file drops. + +- `dags/partners/` — `ManifestHook` and `PartnerManifestSensor`. +- `dags/partner_ingest.py` — the daily ingest DAG. +- `tools/manifest_stub.py` — local stand-in for the manifest API (run it with `--help`). + +## Manifest API + +Connection `manifests_default` (type `http`): `host`, `port`, `schema` = URL scheme, +`password` = bearer token. + +`GET /v1/partners//manifests/` answers `200 {"status": "PENDING"}` or +`200 {"status": "PUBLISHED", "files": [...]}`. A published manifest never changes. +The API takes about a second per call and has short outages (`503`) a few times a week. + +## Local development + +```bash +python tools/manifest_stub.py --publish-after 20 & # prints its port +export AIRFLOW_CONN_MANIFESTS_DEFAULT='{"conn_type": "http", "host": "127.0.0.1", "port": , "password": "eval-manifest-token"}' +``` diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/.airflowignore b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/.airflowignore new file mode 100644 index 0000000..b1ad18c --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/.airflowignore @@ -0,0 +1 @@ +partners/ diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partner_ingest.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partner_ingest.py new file mode 100644 index 0000000..1e73d3b --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partner_ingest.py @@ -0,0 +1,41 @@ +"""Daily ingest of partner manifests.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +from airflow.sdk import dag, task + +from partners.sensors import PartnerManifestSensor + + +@dag( + schedule="0 4 * * *", + start_date=datetime(2026, 9, 1), + catchup=False, + default_args={"retries": 3, "retry_delay": timedelta(minutes=2)}, + tags=["partners"], +) +def partner_ingest(): + @task + def prepare_staging(ds=None): + print(f"staging prepared for {ds}") + + @task + def load(partner: str, files: list[str]): + print(f"loading {len(files)} {partner} files") + return len(files) + + staging = prepare_staging() + # acme feeds finance reporting: wait up to 6 hours, then page. + acme = PartnerManifestSensor(task_id="wait_acme", partner="acme", poke_interval=300, timeout=6 * 3600) + # globex is nice to have: give up quietly after 3 hours. + globex = PartnerManifestSensor( + task_id="wait_globex", partner="globex", poke_interval=300, timeout=3 * 3600, soft_fail=True + ) + staging >> [acme, globex] + load.override(task_id="load_acme")("acme", acme.output) + load.override(task_id="load_globex")("globex", globex.output) + + +partner_ingest() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/__init__.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/hooks.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/hooks.py new file mode 100644 index 0000000..74b3ef7 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/hooks.py @@ -0,0 +1,45 @@ +"""Hook for the partner manifest API. + +GET /v1/partners//manifests/ + 200 {"status": "PENDING"} or {"status": "PUBLISHED", "files": ["s3://...", ...]} + anything else is an error +""" + +from __future__ import annotations + +import requests +from airflow.sdk import BaseHook + + +class ManifestApiError(Exception): + """The manifest API answered with an error or could not be reached.""" + + +class ManifestHook(BaseHook): + """Connection: ``host``/``port`` locate the API, ``schema`` is the URL scheme, ``password`` the token.""" + + conn_name_attr = "manifest_conn_id" + default_conn_name = "manifests_default" + conn_type = "http" + hook_name = "Partner manifests" + + def __init__(self, manifest_conn_id: str = default_conn_name, request_timeout: float = 20.0) -> None: + super().__init__() + self.manifest_conn_id = manifest_conn_id + self.request_timeout = request_timeout + + @staticmethod + def url_and_headers(conn, partner: str, ds: str) -> tuple[str, dict[str, str]]: + port = f":{conn.port}" if conn.port else "" + url = f"{conn.schema or 'http'}://{conn.host}{port}/v1/partners/{partner}/manifests/{ds}" + return url, {"Authorization": f"Bearer {conn.password}"} + + def get_manifest(self, partner: str, ds: str) -> dict: + url, headers = self.url_and_headers(self.get_connection(self.manifest_conn_id), partner, ds) + try: + resp = requests.get(url, headers=headers, timeout=self.request_timeout) + except requests.RequestException as exc: + raise ManifestApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise ManifestApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/sensors.py new file mode 100644 index 0000000..c7084fb --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/dags/partners/sensors.py @@ -0,0 +1,40 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue + +from partners.hooks import ManifestHook + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/requirements.txt b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/requirements.txt new file mode 100644 index 0000000..63ce0cc --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard==1.19.0 +requests==2.34.2 +httpx==0.28.1 diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/tools/manifest_stub.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/tools/manifest_stub.py new file mode 100644 index 0000000..f3dab25 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/fixture/tools/manifest_stub.py @@ -0,0 +1,94 @@ +"""Local stand-in for the partner manifest API. + + python tools/manifest_stub.py --publish-after 10 # prints its URL (a free port unless --port) + +Flags: + --publish-after S manifests become PUBLISHED S seconds after the first request (default: never) + --latency S delay added to every request, like the real API (default 0) + --outage START END every request answers HTTP 503 between START and END seconds after + the first request + --port N, --log FILE listen port; JSONL record of requests +Token: eval-manifest-token. +""" + +from __future__ import annotations + +import argparse +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +TOKEN = "eval-manifest-token" + + +def files_for(partner: str, ds: str) -> list[str]: + return [f"s3://partners/{partner}/{ds}/batch-{i}.csv.gz" for i in range(3)] + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=0) + ap.add_argument("--port-file", default=None) + ap.add_argument("--log", default=None) + ap.add_argument("--latency", type=float, default=0.0) + ap.add_argument("--publish-after", type=float, default=None) + ap.add_argument("--outage", type=float, nargs=2, default=None) + args = ap.parse_args() + lock = threading.Lock() + first = {"t": None} + + def log(**rec): + if args.log: + rec["t"] = time.time() + with lock, open(args.log, "a") as fh: + fh.write(json.dumps(rec) + "\n") + + class H(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *a): + pass + + def _send(self, code, body): + data = json.dumps(body).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + with lock: + if first["t"] is None: + first["t"] = time.time() + if args.latency: + time.sleep(args.latency) + age = time.time() - first["t"] + parts = [p for p in self.path.split("?")[0].split("/") if p] + if self.headers.get("Authorization") != f"Bearer {TOKEN}": + log(event="unauthorized", path=self.path) + return self._send(401, {"error": "unauthorized"}) + if args.outage and args.outage[0] <= age < args.outage[1]: + log(event="503", path=self.path) + return self._send(503, {"error": "service unavailable"}) + if len(parts) == 5 and parts[:2] == ["v1", "partners"] and parts[3] == "manifests": + partner, ds = parts[2], parts[4] + ready = args.publish_after is not None and age >= args.publish_after + log(event="status", partner=partner, ds=ds, ready=ready) + if ready: + return self._send(200, {"status": "PUBLISHED", "files": files_for(partner, ds)}) + return self._send(200, {"status": "PENDING"}) + self._send(404, {"error": "not found"}) + + srv = ThreadingHTTPServer(("127.0.0.1", args.port), H) + srv.daemon_threads = True + if args.port_file: + with open(args.port_file, "w") as fh: + fh.write(str(srv.server_address[1])) + print(f"manifest API listening on http://127.0.0.1:{srv.server_address[1]} (token {TOKEN})", flush=True) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grade.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grade.py new file mode 100644 index 0000000..a82c567 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grade.py @@ -0,0 +1,205 @@ +"""Grader for hard-deferrable-sensor-retry-budget (Airflow 3.3). + +The agent makes PartnerManifestSensor deferrable while keeping `timeout` a per-DAG-run budget +counted from the first try. Grading drives the agent's sensor through hidden scenarios with +grader-only DAGs (grader_data/eval_dags/) against a grader-owned manifest API +(grader_data/manifest_api.py, 0.5 s per request), using `airflow dags test` wrapped by +grader_data/defer_runner.py (deferral timeouts enforced with a 4 s grace, triggerer restart +mid-wait, event-loop stalls measured; see that file). + +Scenarios (each with its own API server and metadata DB, run in parallel): + ok manifest published 4 s after the first request + budget 6 s upstream task, then the sensor (timeout=16 s, retries=4); every request + answers 503 from 3 s to 8 s after the first one; never published + restart timeout=12 s, retries=2, never published, triggerer restarted 8 s in + softfail_outage soft_fail=True, retries=3; 503 from 2 s to 6 s; published at 9 s + softfail_timeout soft_fail=True, timeout=6 s, never published +""" + +from __future__ import annotations + +import concurrent.futures as cf +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +CASE_DIR = Path(os.path.abspath(__file__)).parent # no resolve(): a symlinked case dir stays as given +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +GD = CASE_DIR / "grader_data" +TOKEN = "eval-manifest-token" +LATENCY_S = 0.5 +STALL_FAIL_S = 0.35 +DAY = "2026-09-29" +RUN_TIMEOUT_S = 400 +BUDGET_S, RESTART_TIMEOUT_S = 16.0, 12.0 +GRACE = (-1.5, 5.5) # trigger-side deadline (poll granularity) .. defer timeout + 4 s grace + +SCENARIOS = { + "ok": dict(server=["--publish-after", "4"], dag="eval_manifest_ok"), + "budget": dict(server=["--outage", "3", "8"], dag="eval_manifest_budget"), + "restart": dict(server=[], dag="eval_manifest_restart", env={"EVAL_RESTART_AFTER_S": "8"}), + "softfail_outage": dict(server=["--outage", "2", "6", "--publish-after", "9"], dag="eval_manifest_softfail_outage"), + "softfail_timeout": dict(server=[], dag="eval_manifest_softfail_timeout"), +} + +TI_PROBE = r''' +import sqlite3, os +db = os.environ["AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"].split("sqlite:///", 1)[1] +con = sqlite3.connect(db) +rows = con.execute("SELECT dag_id, task_id, map_index, state, try_number FROM task_instance").fetchall() +RESULT = [dict(zip(["dag_id", "task_id", "map_index", "state", "try_number"], r)) for r in rows] +''' + + +def files_for(partner: str, ds: str) -> list[str]: + return [f"s3://partners/{partner}/{ds}/batch-{i}.csv.gz" for i in range(3)] + + +def read_jsonl(path: Path) -> list[dict]: + out = [] + if path.exists(): + for line in path.read_text().splitlines(): + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + pass + return out + + +def run_scenario(py: str, ws: Path, name: str, spec: dict) -> dict: + tmp = Path(tempfile.mkdtemp(prefix=f"eval-manifest-{name}-")) + port_file, api_log, defer_log, out_dir = tmp / "port", tmp / "api.jsonl", tmp / "defer.jsonl", tmp / "out" + out_dir.mkdir() + server = subprocess.Popen( + [py, str(GD / "manifest_api.py"), "--port-file", str(port_file), "--log", str(api_log), + "--latency", str(LATENCY_S), *spec["server"]], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) + try: + for _ in range(100): + if port_file.exists() and port_file.read_text().strip(): + break + time.sleep(0.1) + port = int(port_file.read_text().strip()) + env = g.airflow_env(ws, env_py=py, extra={ + "AIRFLOW_CONN_MANIFESTS_DEFAULT": json.dumps( + {"conn_type": "http", "host": "127.0.0.1", "port": port, "password": TOKEN}), + "EVAL_OUT_DIR": str(out_dir), + "EVAL_DEFER_LOG": str(defer_log), + **spec.get("env", {}), + }) + db = g.ensure_db(py, env) + if not db.ok: + return {"name": name, "error": "db migrate failed: " + db.tail(20)} + res = g.run_cmd([py, str(GD / "defer_runner.py"), "dags", "test", spec["dag"], DAY], + env=env, cwd=ws, timeout=RUN_TIMEOUT_S) + tis, _ = g.probe_json(py, TI_PROBE, ws, env=env) + outputs = {p.stem: json.loads(p.read_text()) for p in out_dir.glob("*.json")} + return {"name": name, "api": read_jsonl(api_log), "defer": read_jsonl(defer_log), "tis": tis or [], + "outputs": outputs, "log": res.output[-8000:] + ("\n[timed out]" if res.timed_out else "")} + finally: + server.kill() + server.wait() + + +def ti(r: dict, task_id: str = "wait") -> dict: + return next((t for t in r.get("tis", []) if t["task_id"] == task_id), {}) + + +def recs(r: dict, kind: str, task_id: str = "wait") -> list[dict]: + return [e for e in r.get("defer", []) if e["kind"] == kind and e.get("task_id") == task_id] + + +def wait_span(r: dict) -> float | None: + """Seconds from the sensor's first worker start to its last task end.""" + starts, ends = recs(r, "worker_run"), recs(r, "task_end") + return round(ends[-1]["t"] - starts[0]["t"], 2) if starts and ends else None + + +def brief(r: dict) -> str: + ev = [{k: v for k, v in e.items() if k in ("kind", "task_id", "try_number", "waited_s", "payload", "timeout_s", + "state", "via")} + for e in r.get("defer", []) if e.get("task_id") == "wait"] + n503 = sum(1 for e in r.get("api", []) if e.get("event") == "503") + return json.dumps({"tis": [(t["task_id"], t["state"], t["try_number"]) for t in r.get("tis", [])], + "outputs": r.get("outputs"), "api_503s": n503, "events": ev[:16]}, default=str)[:2500] + + +def log_tail(r: dict, n: int = 2000) -> str: + keep = [ln for ln in r.get("log", "").splitlines() + if any(k in ln for k in ("Error", "Exception", "Traceback", "raise ", "File \"", "timed out"))] + return "\n".join(keep)[-n:] + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + penv = g.airflow_env(ws, env_py=py) + penv["PYTHONPATH"] = os.pathsep.join((str(ws / "dags"), penv["PYTHONPATH"])) + imp = g.import_dags(py, ws, env=penv) + errs = "; ".join(f"{k}: {v.strip().splitlines()[-1]}" for k, v in imp["import_errors"].items()) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"] and "partner_ingest" in imp["dags"], + imp["probe_error"] or errs or f"dags: {sorted(imp['dags'])}") + + run_ws = g.copy_workspace(ws) + shutil.copy2(GD / "eval_dags" / "_eval_manifest_scenarios.py", run_ws / "dags" / "_eval_manifest_scenarios.py") + with cf.ThreadPoolExecutor(max_workers=len(SCENARIOS)) as ex: + futs = {n: ex.submit(run_scenario, py, run_ws, n, s) for n, s in SCENARIOS.items()} + R = {n: f.result() for n, f in futs.items()} + + o = R["ok"] + deferred = bool(recs(o, "defer")) + ok = ti(o).get("state") == "success" and o["outputs"].get("eval_manifest_ok") == files_for("acme", DAY) + grader.primary("sensor defers and returns the manifest's file list", ok and deferred, + f"deferred={deferred}\n" + brief(o) + ("" if ok else "\n" + log_tail(o))) + + stalls = sorted((s for r in R.values() for e in r.get("defer", []) if e["kind"].startswith("trigger_") + for s in e.get("stalls", [])), reverse=True) + big = [s for s in stalls if s >= STALL_FAIL_S] + ran = sum(1 for r in R.values() for e in r.get("defer", []) if e["kind"].startswith("trigger_")) + grader.primary("trigger never blocks the event loop (API answers in 0.5 s)", ran > 0 and len(big) < 2, + f"{ran} trigger runs; stalls >= {STALL_FAIL_S}s: {big[:10]}") + + b = R["budget"] + span = wait_span(b) + tries = ti(b).get("try_number", 0) + lo, hi = BUDGET_S + GRACE[0], BUDGET_S + GRACE[1] + ok = ti(b).get("state") == "failed" and tries >= 2 and span is not None and lo <= span <= hi + grader.primary("timeout is one budget per DAG run: retries after an outage do not reset it", ok, + f"sensor ran from first try start to final failure in {span}s over {tries} tries " + f"(timeout={BUDGET_S}s, want {lo}-{hi}s)\n" + brief(b) + ("" if ok else "\n" + log_tail(b))) + + rs = R["restart"] + span = wait_span(rs) + lo, hi = RESTART_TIMEOUT_S + GRACE[0], RESTART_TIMEOUT_S + GRACE[1] + restarted = bool(recs(rs, "restart")) + ok_fail = ti(rs).get("state") == "failed" and ti(rs).get("try_number") == 1 + grader.primary("a timed-out sensor fails for good (no retries)", ok_fail, brief(rs) + ("" if ok_fail else "\n" + log_tail(rs))) + grader.primary("triggerer restart does not extend the timeout", restarted and ok_fail and span is not None + and lo <= span <= hi, + f"restart happened={restarted}; ended {span}s after the sensor started (want {lo}-{hi}s)\n" + brief(rs)) + + so = R["softfail_outage"] + ok = ti(so).get("state") == "success" and so["outputs"].get("eval_manifest_softfail_outage") == files_for("acme", DAY) + grader.primary("soft_fail sensor uses its retries for an API outage instead of skipping", ok, + brief(so) + ("" if ok else "\n" + log_tail(so))) + + st = R["softfail_timeout"] + ok = ti(st).get("state") == "skipped" + grader.primary("soft_fail sensor is skipped when its timeout runs out", ok, brief(st) + ("" if ok else "\n" + log_tail(st))) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/defer_runner.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/defer_runner.py new file mode 100644 index 0000000..43f9fae --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/defer_runner.py @@ -0,0 +1,290 @@ +"""`airflow dags test` with the production triggerer/scheduler behaviours it skips (Airflow 3.3.2). + +Usage: python defer_runner.py dags test + +Plain `dags test` runs each trigger inline until its first event. That matches production +for the happy path (the trigger is rebuilt from ``serialize()``'s classpath + kwargs and the +task resumes in a fresh operator instance), but skips things a real deployment does. This +wrapper replaces ``airflow.sdk.definitions.dag._run_task`` with a copy that adds them, each +modelled on airflow/jobs/triggerer_job_runner.py, models/trigger.py and the scheduler: + +* **Deferral timeouts.** ``defer(timeout=...)`` is enforced like the scheduler's + ``check_trigger_timeouts``: once it passes (plus ``EVAL_DEFER_TIMEOUT_GRACE_S``, standing in + for the scheduler's 15 s check interval) the trigger is cancelled (no ``on_kill``) and the + task resumes with ``next_method="__fail__"``, ``error=TRIGGER_TIMEOUT``. +* **Triggerer restarts** (``EVAL_RESTART_AFTER_S``): after that many seconds the running trigger + is cancelled without a message (the shutdown path: no ``on_kill``), ``cleanup()`` is awaited, + and a new instance is built from the classpath + kwargs stored at deferral time. +* **User action** (``EVAL_USER_KILL_AFTER_S``): after that many seconds the task instance is + marked failed in the metadata DB (what "mark failed" in the UI does), the trigger task is + cancelled with the triggerer's user-action message, ``on_kill()`` is awaited (30 s bound), + then ``cleanup()``. The task does not resume. +* **A trigger that raises or ends without an event** fails the task with + ``error=TRIGGER_FAILURE``, as the triggerer does. +* **Task-end events.** A ``TaskSuccessEvent``/``TaskFailedEvent``/``TaskSkippedEvent`` from the + trigger is applied with the production handler (``models.trigger.handle_event_submit``): + the task instance gets its final state and XComs without resuming on a worker. +* **Event + defer kwargs.** The resume method gets ``defer(kwargs=...)`` merged with + ``event=`` as in production; plain ``dags test`` drops the defer kwargs. +* The trigger's ``task_instance`` is set (dag_id, task_id, run_id, map_index, try_number) and + the task instance stays ``deferred`` in the DB while the trigger runs, as in production. + +It also measures event-loop stalls while triggers run (a 20 ms ticker; gaps over +``STALL_RECORD_S`` are recorded) and writes JSONL records to ``EVAL_DEFER_LOG``. +A trigger still running after ``EVAL_TRIGGER_HARD_CAP_S`` is cancelled and recorded as a hang. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import json +import os +import sys +import time +import traceback +from uuid import UUID + +LOG_PATH = os.environ.get("EVAL_DEFER_LOG") +RESTART_AFTER_S = float(os.environ.get("EVAL_RESTART_AFTER_S") or 0) +USER_KILL_AFTER_S = float(os.environ.get("EVAL_USER_KILL_AFTER_S") or 0) +GRACE_S = float(os.environ.get("EVAL_DEFER_TIMEOUT_GRACE_S") or 4) +HARD_CAP_S = float(os.environ.get("EVAL_TRIGGER_HARD_CAP_S") or 120) +ON_KILL_TIMEOUT_S = 30 +USER_ACTION_CANCEL_MSG = "__airflow_user_action__" # triggerer_job_runner._USER_ACTION_CANCEL_MSG +STALL_RECORD_S = 0.15 +TICK_S = 0.02 + + +def record(**rec) -> None: + rec["t"] = time.time() + if LOG_PATH: + with open(LOG_PATH, "a") as fh: + fh.write(json.dumps(rec, default=str) + "\n") + + +def _drive_trigger(msg, task_sdk_ti, label: dict, mark_failed): + """Run the deferred trigger like a triggerer would. + + Returns (next_method, next_kwargs), or None when a user action ended the task. + """ + import psutil + import structlog + + from airflow.sdk._shared.module_loading import import_string + from airflow.sdk.bases.operator import TRIGGER_FAIL_REPR, TriggerFailureReason + from airflow.sdk.execution_time.supervisor import ( + InProcessSupervisorComms, + InProcessTestSupervisor, + set_supervisor_comms, + ) + from airflow.sdk.serde import deserialize, serialize + + timeout_s = msg.trigger_timeout.total_seconds() if msg.trigger_timeout is not None else None + deferred_at = time.time() + fail_at = deferred_at + timeout_s + GRACE_S if timeout_s is not None else None + + def build(): + trig = import_string(msg.classpath)(**deserialize(msg.trigger_kwargs)) + with contextlib.suppress(Exception): + trig.task_instance = task_sdk_ti + return trig + + async def first_event(trig): + return await anext(trig.run(), None) + + async def stop(task, trig, message=None): + task.cancel(message) if message else task.cancel() + with contextlib.suppress(BaseException): + await task + + async def main(): + loop = asyncio.get_running_loop() + stalls: list[float] = [] + + async def monitor(): + last = loop.time() + while True: + await asyncio.sleep(TICK_S) + now = loop.time() + gap = now - last - TICK_S + if gap > STALL_RECORD_S: + stalls.append(round(gap, 3)) + last = now + + mon = asyncio.create_task(monitor()) + trig = build() + task = asyncio.create_task(first_event(trig)) + outcome: tuple[str, object] = ("hang", None) + try: + if RESTART_AFTER_S: + done, _ = await asyncio.wait({task}, timeout=RESTART_AFTER_S) + if task not in done: + await stop(task, trig) + with contextlib.suppress(Exception): + await trig.cleanup() + record(kind="restart", **label) + trig = build() + task = asyncio.create_task(first_event(trig)) + limit = HARD_CAP_S + if fail_at is not None: + limit = min(limit, max(0.0, fail_at - time.time())) + user_kill = bool(USER_KILL_AFTER_S) and USER_KILL_AFTER_S < limit + done, _ = await asyncio.wait({task}, timeout=USER_KILL_AFTER_S if user_kill else limit) + if task in done: + try: + outcome = ("event", task.result()) + except BaseException as exc: # noqa: BLE001 - mirrors the triggerer + outcome = ("error", "".join(traceback.format_exception(exc))) + elif user_kill: + mark_failed() + record(kind="user_kill", **label) + await stop(task, trig, USER_ACTION_CANCEL_MSG) + try: + await asyncio.wait_for(trig.on_kill(), timeout=ON_KILL_TIMEOUT_S) + except Exception as exc: # noqa: BLE001 - the triggerer only logs these + record(kind="on_kill_error", error=repr(exc), **label) + outcome = ("user_kill", None) + else: + await stop(task, trig) + outcome = ("timeout", None) if fail_at is not None and time.time() >= fail_at - 0.05 else ("hang", None) + finally: + with contextlib.suppress(Exception): + await trig.cleanup() + mon.cancel() + return outcome, stalls + + supervisor = InProcessTestSupervisor( + id=task_sdk_ti.id, + pid=os.getpid(), + process=psutil.Process(), + process_log=structlog.get_logger(logger_name="task").bind(), + client=InProcessTestSupervisor._api_client(), + ) + supervisor.comms = InProcessSupervisorComms(supervisor=supervisor) + with set_supervisor_comms(supervisor.comms): + (kind, value), stalls = asyncio.run(main()) + + rec = dict(kind="trigger_" + kind, waited_s=round(time.time() - deferred_at, 3), stalls=stalls, **label) + if kind == "user_kill": + record(**rec) + return None + if kind == "event": + if value is None: + rec["kind"] = "trigger_no_event" + record(**rec) + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, + "traceback": ["trigger exited without yielding an event"]} + rec["payload"] = value.payload + rec["event_type"] = type(value).__name__ + record(**rec) + from airflow.triggers.base import BaseTaskEndEvent + + if isinstance(value, BaseTaskEndEvent): + return ("end_event", value) + # Production (models/trigger.py handle_event_submit) merges the event into the defer + # kwargs; plain `dags test` 3.3.2 replaces them with {"event": ...}. + next_kwargs = dict(deserialize(msg.next_kwargs) or {}) if msg.next_kwargs else {} + next_kwargs["event"] = value.payload + return msg.next_method, serialize(next_kwargs) + if kind == "error": + rec["traceback"] = value + record(**rec) + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, "traceback": str(value).splitlines()} + record(**rec) + if kind == "timeout": + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_TIMEOUT} + return TRIGGER_FAIL_REPR, {"error": TriggerFailureReason.TRIGGER_FAILURE, + "traceback": [f"eval: trigger did not fire within {HARD_CAP_S}s"]} + + +def _run_task(*, ti, task, run_triggerer: bool = False): + """Copy of airflow.sdk.definitions.dag._run_task (3.3.2) with the inline trigger replaced.""" + import airflow.sdk.definitions.dag as dagmod + from airflow.sdk.api.datamodels._generated import TaskInstance as TaskInstanceSDK + from airflow.sdk.execution_time.comms import DeferTask + from airflow.sdk.execution_time.supervisor import run_task_in_process + from airflow.serialization.serialized_objects import create_scheduler_operator + from airflow.utils.session import create_session + from airflow.utils.state import TaskInstanceState + + log = dagmod.log + taskrun_result = None + log.info("[DAG TEST] starting task_id=%s map_index=%s", ti.task_id, ti.map_index) + while True: + try: + ti.set_state(TaskInstanceState.QUEUED) + task_sdk_ti = TaskInstanceSDK( + id=UUID(str(ti.id)), + task_id=ti.task_id, + dag_id=ti.dag_id, + run_id=ti.run_id, + try_number=ti.try_number, + map_index=ti.map_index, + dag_version_id=UUID(str(ti.dag_version_id)), + ) + record(kind="worker_run", task_id=ti.task_id, map_index=ti.map_index, try_number=ti.try_number, + run_id=ti.run_id) + taskrun_result = run_task_in_process(ti=task_sdk_ti, task=task) + msg = taskrun_result.msg + ti.set_state(taskrun_result.ti.state) + ti.task = create_scheduler_operator(taskrun_result.ti.task) + label = {"task_id": ti.task_id, "map_index": ti.map_index, "try_number": ti.try_number, + "run_id": ti.run_id} + if ti.state == TaskInstanceState.DEFERRED and isinstance(msg, DeferTask) and run_triggerer: + record(kind="defer", classpath=msg.classpath, trigger_kwargs=msg.trigger_kwargs, + timeout_s=msg.trigger_timeout.total_seconds() if msg.trigger_timeout else None, + next_method=msg.next_method, next_kwargs=msg.next_kwargs, **label) + resumed = _drive_trigger(msg, task_sdk_ti, label, + mark_failed=lambda: ti.set_state(TaskInstanceState.FAILED)) + if resumed is None: + ti.set_state(TaskInstanceState.FAILED) + record(kind="task_end", state=str(ti.state), **label) + break + if resumed[0] == "end_event": + # The trigger ended the task itself (TaskSuccessEvent & co): apply it with the + # production handler (models/trigger.py), no resume on a worker. + from airflow.models.taskinstance import TaskInstance as TIModel + from airflow.models.trigger import handle_event_submit + + with create_session() as session: + db_ti = session.get(TIModel, ti.id) + handle_event_submit(resumed[1], task_instance=db_ti, session=session) + final_state = db_ti.state + ti.refresh_from_db() + record(kind="task_end", state=str(final_state), via="trigger_end_event", **label) + if final_state == TaskInstanceState.UP_FOR_RETRY: + taskrun_result = None + break + ti.set_state(TaskInstanceState.QUEUED) + ti.next_method, ti.next_kwargs = resumed + with create_session() as session: + ti.state = TaskInstanceState.SCHEDULED + session.add(ti) + continue + record(kind="task_end", state=str(ti.state), **label) + break + except Exception: + log.exception("[DAG TEST] Error running task %s", ti) + record(kind="harness_exception", traceback=traceback.format_exc()) + if ti.state not in dagmod.FINISHED_STATES: + ti.set_state(TaskInstanceState.FAILED) + taskrun_result = None + break + raise + log.info("[DAG TEST] end task task_id=%s map_index=%s", ti.task_id, ti.map_index) + return taskrun_result + + +def main() -> None: + import airflow.sdk.definitions.dag as dagmod + + dagmod._run_task = _run_task + from airflow.__main__ import main as airflow_main + + sys.argv = ["airflow", *sys.argv[1:]] + airflow_main() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/eval_dags/_eval_manifest_scenarios.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/eval_dags/_eval_manifest_scenarios.py new file mode 100644 index 0000000..ea6e0f6 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/eval_dags/_eval_manifest_scenarios.py @@ -0,0 +1,42 @@ +"""Grader-only DAGs that drive the agent's PartnerManifestSensor through hidden scenarios.""" + +from __future__ import annotations + +import json +import os +import time +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +from partners.sensors import PartnerManifestSensor + + +@task +def collect(name: str, files, ds=None): + (Path(os.environ["EVAL_OUT_DIR"]) / f"{name}.json").write_text(json.dumps(files)) + + +@task +def staging(seconds: float): + time.sleep(seconds) + + +def _dag(dag_id, *, timeout, retries=0, soft_fail=False, staging_s=0.0): + @dag(dag_id=dag_id, schedule=None, start_date=datetime(2026, 9, 1), catchup=False, + default_args={"retries": retries, "retry_delay": timedelta(seconds=2)}) + def _d(): + s = PartnerManifestSensor(task_id="wait", partner="acme", poke_interval=1, timeout=timeout, + soft_fail=soft_fail) + staging(staging_s) >> s + collect(dag_id, s.output) + + return _d() + + +_dag("eval_manifest_ok", timeout=60) +_dag("eval_manifest_budget", timeout=16, retries=4, staging_s=6.0) +_dag("eval_manifest_restart", timeout=12, retries=2) +_dag("eval_manifest_softfail_outage", timeout=60, retries=3, soft_fail=True) +_dag("eval_manifest_softfail_timeout", timeout=6, retries=2, soft_fail=True) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/manifest_api.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/manifest_api.py new file mode 100644 index 0000000..f3dab25 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/grader_data/manifest_api.py @@ -0,0 +1,94 @@ +"""Local stand-in for the partner manifest API. + + python tools/manifest_stub.py --publish-after 10 # prints its URL (a free port unless --port) + +Flags: + --publish-after S manifests become PUBLISHED S seconds after the first request (default: never) + --latency S delay added to every request, like the real API (default 0) + --outage START END every request answers HTTP 503 between START and END seconds after + the first request + --port N, --log FILE listen port; JSONL record of requests +Token: eval-manifest-token. +""" + +from __future__ import annotations + +import argparse +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +TOKEN = "eval-manifest-token" + + +def files_for(partner: str, ds: str) -> list[str]: + return [f"s3://partners/{partner}/{ds}/batch-{i}.csv.gz" for i in range(3)] + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=0) + ap.add_argument("--port-file", default=None) + ap.add_argument("--log", default=None) + ap.add_argument("--latency", type=float, default=0.0) + ap.add_argument("--publish-after", type=float, default=None) + ap.add_argument("--outage", type=float, nargs=2, default=None) + args = ap.parse_args() + lock = threading.Lock() + first = {"t": None} + + def log(**rec): + if args.log: + rec["t"] = time.time() + with lock, open(args.log, "a") as fh: + fh.write(json.dumps(rec) + "\n") + + class H(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *a): + pass + + def _send(self, code, body): + data = json.dumps(body).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + with lock: + if first["t"] is None: + first["t"] = time.time() + if args.latency: + time.sleep(args.latency) + age = time.time() - first["t"] + parts = [p for p in self.path.split("?")[0].split("/") if p] + if self.headers.get("Authorization") != f"Bearer {TOKEN}": + log(event="unauthorized", path=self.path) + return self._send(401, {"error": "unauthorized"}) + if args.outage and args.outage[0] <= age < args.outage[1]: + log(event="503", path=self.path) + return self._send(503, {"error": "service unavailable"}) + if len(parts) == 5 and parts[:2] == ["v1", "partners"] and parts[3] == "manifests": + partner, ds = parts[2], parts[4] + ready = args.publish_after is not None and age >= args.publish_after + log(event="status", partner=partner, ds=ds, ready=ready) + if ready: + return self._send(200, {"status": "PUBLISHED", "files": files_for(partner, ds)}) + return self._send(200, {"status": "PENDING"}) + self._send(404, {"error": "not found"}) + + srv = ThreadingHTTPServer(("127.0.0.1", args.port), H) + srv.daemon_threads = True + if args.port_file: + with open(args.port_file, "w") as fh: + fh.write(str(srv.server_address[1])) + print(f"manifest API listening on http://127.0.0.1:{srv.server_address[1]} (token {TOKEN})", flush=True) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_dag_run_start/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_dag_run_start/dags/partners/sensors.py new file mode 100644 index 0000000..525c879 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_dag_run_start/dags/partners/sensors.py @@ -0,0 +1,83 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + started = context["dag_run"].start_date.isoformat() + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_in_xcom/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_in_xcom/dags/partners/sensors.py new file mode 100644 index 0000000..4b58f34 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_in_xcom/dags/partners/sensors.py @@ -0,0 +1,87 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + ti = context["ti"] + started = ti.xcom_pull(task_ids=ti.task_id, key=WAIT_STARTED_KEY) + if started is None: + started = datetime.now(timezone.utc).isoformat() + ti.xcom_push(key=WAIT_STARTED_KEY, value=started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_this_try/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_this_try/dags/partners/sensors.py new file mode 100644 index 0000000..fc79ca6 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/anchor_this_try/dags/partners/sensors.py @@ -0,0 +1,83 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + started = datetime.now(timezone.utc).isoformat() + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/api_error_airflow_exception/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/api_error_airflow_exception/dags/partners/sensors.py new file mode 100644 index 0000000..68656ba --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/api_error_airflow_exception/dags/partners/sensors.py @@ -0,0 +1,87 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowException, AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + store = context["task_state_store"] + started = store.get(WAIT_STARTED_KEY) + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set(WAIT_STARTED_KEY, started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise AirflowException(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/deferrable_flag_ignored/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/deferrable_flag_ignored/dags/partners/sensors.py new file mode 100644 index 0000000..e3539ef --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/deferrable_flag_ignored/dags/partners/sensors.py @@ -0,0 +1,43 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue + +from partners.hooks import ManifestHook + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + # MUTANT (adversarial no-op): flag accepted and stored, sensor still pokes from a worker. + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/hardcoded_file_list/dags/partners/triggers.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/hardcoded_file_list/dags/partners/triggers.py new file mode 100644 index 0000000..ec5a3df --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/hardcoded_file_list/dags/partners/triggers.py @@ -0,0 +1,52 @@ +"""Trigger that waits for a partner manifest.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from partners.hooks import ManifestApiError, ManifestHook + + +class ManifestPublishedTrigger(BaseTrigger): + """Poll until the manifest is PUBLISHED, the API fails, or the absolute ``deadline`` passes.""" + + def __init__(self, partner: str, ds: str, deadline: datetime, manifest_conn_id: str, + poll_interval: float = 60.0) -> None: + super().__init__() + self.partner = partner + self.ds = ds + self.deadline = deadline + self.manifest_conn_id = manifest_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ("partners.triggers.ManifestPublishedTrigger", { + "partner": self.partner, "ds": self.ds, "deadline": self.deadline, + "manifest_conn_id": self.manifest_conn_id, "poll_interval": self.poll_interval, + }) + + async def run(self): + # MUTANT (adversarial): never asks the API, fabricates the expected file list. + await asyncio.sleep(self.poll_interval) + yield TriggerEvent({"status": "published", + "files": [f"s3://partners/{self.partner}/{self.ds}/batch-{i}.csv.gz" for i in range(3)]}) + return + hook = ManifestHook(self.manifest_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + manifest = await hook.aget_manifest(self.partner, self.ds) + except ManifestApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if manifest["status"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": manifest["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/sensors.py new file mode 100644 index 0000000..31ecb2c --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/sensors.py @@ -0,0 +1,86 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + store = context["task_state_store"] + started = store.get(WAIT_STARTED_KEY) + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set(WAIT_STARTED_KEY, started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/triggers.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/triggers.py new file mode 100644 index 0000000..57a2efb --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/relative_deadline_no_defer_timeout/dags/partners/triggers.py @@ -0,0 +1,48 @@ +"""Trigger that waits for a partner manifest.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from partners.hooks import ManifestApiError, ManifestHook + + +class ManifestPublishedTrigger(BaseTrigger): + """Poll until the manifest is PUBLISHED, the API fails, or the absolute ``deadline`` passes.""" + + def __init__(self, partner: str, ds: str, deadline: datetime, manifest_conn_id: str, + poll_interval: float = 60.0) -> None: + super().__init__() + self.partner = partner + self.ds = ds + self.timeout_s = deadline if isinstance(deadline, (int, float)) else (deadline - datetime.now(timezone.utc)).total_seconds() + self.deadline = datetime.now(timezone.utc) + timedelta(seconds=self.timeout_s) + self.manifest_conn_id = manifest_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ("partners.triggers.ManifestPublishedTrigger", { + "partner": self.partner, "ds": self.ds, "deadline": self.timeout_s, + "manifest_conn_id": self.manifest_conn_id, "poll_interval": self.poll_interval, + }) + + async def run(self): + hook = ManifestHook(self.manifest_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + manifest = await hook.aget_manifest(self.partner, self.ds) + except ManifestApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if manifest["status"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": manifest["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/sync_hook_in_trigger/dags/partners/triggers.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/sync_hook_in_trigger/dags/partners/triggers.py new file mode 100644 index 0000000..c87569d --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/sync_hook_in_trigger/dags/partners/triggers.py @@ -0,0 +1,47 @@ +"""Trigger that waits for a partner manifest.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from partners.hooks import ManifestApiError, ManifestHook + + +class ManifestPublishedTrigger(BaseTrigger): + """Poll until the manifest is PUBLISHED, the API fails, or the absolute ``deadline`` passes.""" + + def __init__(self, partner: str, ds: str, deadline: datetime, manifest_conn_id: str, + poll_interval: float = 60.0) -> None: + super().__init__() + self.partner = partner + self.ds = ds + self.deadline = deadline + self.manifest_conn_id = manifest_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ("partners.triggers.ManifestPublishedTrigger", { + "partner": self.partner, "ds": self.ds, "deadline": self.deadline, + "manifest_conn_id": self.manifest_conn_id, "poll_interval": self.poll_interval, + }) + + async def run(self): + hook = ManifestHook(self.manifest_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + manifest = hook.get_manifest(self.partner, self.ds) + except ManifestApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if manifest["status"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": manifest["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/variable_anchor_non_string/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/variable_anchor_non_string/dags/partners/sensors.py new file mode 100644 index 0000000..edfc6e1 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/mutants/variable_anchor_non_string/dags/partners/sensors.py @@ -0,0 +1,89 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue, Variable +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + ti = context["ti"] + key = f"manifest_wait_start__{ti.dag_id}__{ti.run_id}__{ti.task_id}__{ti.map_index}" + stamp = Variable.get(key, default=None) + if stamp is None: + stamp = datetime.now(timezone.utc).timestamp() + Variable.set(key, stamp) # 3.3 Task SDK: Variable values must be str + started = datetime.fromtimestamp(float(stamp), timezone.utc).isoformat() + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/hooks.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/hooks.py new file mode 100644 index 0000000..515693d --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/hooks.py @@ -0,0 +1,58 @@ +"""Hook for the partner manifest API. + +GET /v1/partners//manifests/ + 200 {"status": "PENDING"} or {"status": "PUBLISHED", "files": ["s3://...", ...]} + anything else is an error +""" + +from __future__ import annotations + +import httpx +import requests +from airflow.sdk import BaseHook + + +class ManifestApiError(Exception): + """The manifest API answered with an error or could not be reached.""" + + +class ManifestHook(BaseHook): + """Connection: ``host``/``port`` locate the API, ``schema`` is the URL scheme, ``password`` the token.""" + + conn_name_attr = "manifest_conn_id" + default_conn_name = "manifests_default" + conn_type = "http" + hook_name = "Partner manifests" + + def __init__(self, manifest_conn_id: str = default_conn_name, request_timeout: float = 20.0) -> None: + super().__init__() + self.manifest_conn_id = manifest_conn_id + self.request_timeout = request_timeout + + @staticmethod + def url_and_headers(conn, partner: str, ds: str) -> tuple[str, dict[str, str]]: + port = f":{conn.port}" if conn.port else "" + url = f"{conn.schema or 'http'}://{conn.host}{port}/v1/partners/{partner}/manifests/{ds}" + return url, {"Authorization": f"Bearer {conn.password}"} + + def get_manifest(self, partner: str, ds: str) -> dict: + url, headers = self.url_and_headers(self.get_connection(self.manifest_conn_id), partner, ds) + try: + resp = requests.get(url, headers=headers, timeout=self.request_timeout) + except requests.RequestException as exc: + raise ManifestApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise ManifestApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() + + async def aget_manifest(self, partner: str, ds: str) -> dict: + """Async variant for triggers.""" + url, headers = self.url_and_headers(await self.aget_connection(self.manifest_conn_id), partner, ds) + try: + async with httpx.AsyncClient(timeout=self.request_timeout) as client: + resp = await client.get(url, headers=headers) + except httpx.HTTPError as exc: + raise ManifestApiError(f"GET {url} failed: {exc!r}") from exc + if resp.status_code != 200: + raise ManifestApiError(f"GET {url} -> HTTP {resp.status_code}") + return resp.json() diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/sensors.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/sensors.py new file mode 100644 index 0000000..d3124a1 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/sensors.py @@ -0,0 +1,87 @@ +"""Sensor that waits for a partner's daily manifest.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from typing import Any + +from airflow.sdk import BaseSensorOperator, PokeReturnValue +from airflow.sdk.exceptions import AirflowSensorTimeout, AirflowSkipException + +from partners.hooks import ManifestApiError, ManifestHook +from partners.triggers import ManifestPublishedTrigger + +WAIT_STARTED_KEY = "manifest_wait_started_at" + + +class PartnerManifestSensor(BaseSensorOperator): + """Wait until the partner has published the manifest for ``ds`` and return its file list. + + ``timeout`` is a budget per DAG run, counted from the first try's start. The start time + lives in the task state store (Airflow 3.3), which keeps its rows across retries of the + task instance; XComs are cleared when a task instance is retried. + + :param partner: partner key, e.g. ``acme`` + :param ds: manifest date (templated, usually ``{{ ds }}``) + :param manifest_conn_id: connection to the manifest API + :param deferrable: wait in the triggerer (default) instead of poking from a worker + """ + + template_fields: Sequence[str] = ("partner", "ds") + + def __init__( + self, + *, + partner: str, + ds: str = "{{ ds }}", + manifest_conn_id: str = ManifestHook.default_conn_name, + deferrable: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.partner = partner + self.ds = ds + self.manifest_conn_id = manifest_conn_id + self.deferrable = deferrable + + def poke(self, context) -> PokeReturnValue | bool: + manifest = ManifestHook(self.manifest_conn_id).get_manifest(self.partner, self.ds) + if manifest["status"] == "PUBLISHED": + return PokeReturnValue(is_done=True, xcom_value=manifest["files"]) + return False + + def _gave_up(self): + msg = f"{self.partner} manifest for {self.ds} not published within {self.timeout}s" + if self.soft_fail: + raise AirflowSkipException(msg) + raise AirflowSensorTimeout(msg) + + def execute(self, context) -> Any: + if not self.deferrable: + return super().execute(context) + store = context["task_state_store"] + started = store.get(WAIT_STARTED_KEY) + if started is None: + started = datetime.now(timezone.utc).isoformat() + store.set(WAIT_STARTED_KEY, started) + deadline = datetime.fromisoformat(started) + timedelta(seconds=self.timeout) + remaining = deadline - datetime.now(timezone.utc) + if remaining <= timedelta(0): + self._gave_up() + self.defer( + trigger=ManifestPublishedTrigger(partner=self.partner, ds=self.ds, deadline=deadline, + manifest_conn_id=self.manifest_conn_id, + poll_interval=self.poke_interval), + method_name="execute_complete", + timeout=remaining, + ) + + def execute_complete(self, context, event: dict[str, Any]) -> list[str]: + if event["status"] == "published": + return event["files"] + if event["status"] == "timeout": + self._gave_up() + # Not an AirflowException: with soft_fail=True the sensor base class would turn it + # into a skip, but an outage should use the task's retries. + raise ManifestApiError(f"manifest API error for {self.partner}/{self.ds}: {event.get('message')}") diff --git a/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/triggers.py b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/triggers.py new file mode 100644 index 0000000..a6fc118 --- /dev/null +++ b/evals/airflow/cases/hard-deferrable-sensor-retry-budget/reference/dags/partners/triggers.py @@ -0,0 +1,47 @@ +"""Trigger that waits for a partner manifest.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent + +from partners.hooks import ManifestApiError, ManifestHook + + +class ManifestPublishedTrigger(BaseTrigger): + """Poll until the manifest is PUBLISHED, the API fails, or the absolute ``deadline`` passes.""" + + def __init__(self, partner: str, ds: str, deadline: datetime, manifest_conn_id: str, + poll_interval: float = 60.0) -> None: + super().__init__() + self.partner = partner + self.ds = ds + self.deadline = deadline + self.manifest_conn_id = manifest_conn_id + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ("partners.triggers.ManifestPublishedTrigger", { + "partner": self.partner, "ds": self.ds, "deadline": self.deadline, + "manifest_conn_id": self.manifest_conn_id, "poll_interval": self.poll_interval, + }) + + async def run(self): + hook = ManifestHook(self.manifest_conn_id) + while True: + remaining = (self.deadline - datetime.now(timezone.utc)).total_seconds() + if remaining <= 0: + yield TriggerEvent({"status": "timeout"}) + return + try: + manifest = await hook.aget_manifest(self.partner, self.ds) + except ManifestApiError as exc: + yield TriggerEvent({"status": "error", "message": str(exc)}) + return + if manifest["status"] == "PUBLISHED": + yield TriggerEvent({"status": "published", "files": manifest["files"]}) + return + await asyncio.sleep(min(self.poll_interval, remaining)) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_intake_hourly.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_intake_hourly.py new file mode 100644 index 0000000..3c598df --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_intake_hourly.py @@ -0,0 +1,54 @@ +"""Hourly intake of clearing-house claims into service-date partitions. + +Every claim received during the hour is appended to +output/lake/claims/service_date=/received_.csv, and every +partition touched is attached to the ``claims-by-service-date`` alias with the +number of claims it received. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data", "retries": 2}, + tags=["claims", "ingest"], +) +def claims_intake_hourly(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def land_claims(data_interval_start=None, data_interval_end=None, outlet_events=None) -> int: + lo = data_interval_start.strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + by_day: dict[str, list[dict]] = defaultdict(list) + for row in ClaimsExportHook().read("claims_export.csv"): + if lo <= row["received_at"] < hi: + by_day[row["service_date"]].append(row) + for day, rows in sorted(by_day.items()): + path = LAKE / "claims" / f"service_date={day}" / f"received_{data_interval_start.strftime('%Y%m%dT%H')}.csv" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(rows)}) + return sum(len(r) for r in by_day.values()) + + land_claims() + + +claims_intake_hourly() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_reprocess.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_reprocess.py new file mode 100644 index 0000000..fd31c95 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_reprocess.py @@ -0,0 +1,71 @@ +"""On-demand re-check of one service-date partition (triggered by claims ops). + +Trigger with config ``{"service_date": "YYYY-MM-DD"}``. Re-reads the export +for every claim of that service date received before the trigger time, writes +any claim missing from the partition to received_reprocess_.csv, +attaches the partition to the alias so the marts are rebuilt, and logs the +recheck (with the trigger time) to output/marts/_RECHECK_LOG. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import Param, dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=None, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + params={"service_date": Param("2026-03-01", type="string", format="date")}, + default_args={"owner": "claims-ops"}, + tags=["claims", "ops"], +) +def claims_reprocess(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def recheck_partition(outlet_events=None) -> int: + from airflow.sdk import get_current_context + + ctx = get_current_context() + params = ctx["params"] + logical_date = pendulum.instance(ctx["dag_run"].run_after) if ctx.get("logical_date") is None \ + else ctx["logical_date"] + day = params["service_date"] + cutoff = logical_date.strftime("%Y-%m-%d %H:%M:%S") + part = LAKE / "claims" / f"service_date={day}" + landed = set() + for path in part.glob("*.csv"): + with path.open(newline="") as fh: + landed |= {r["claim_id"] for r in csv.DictReader(fh)} + missing = [r for r in ClaimsExportHook().read("claims_export.csv") + if r["service_date"] == day and r["received_at"] < cutoff and r["claim_id"] not in landed] + if missing: + part.mkdir(parents=True, exist_ok=True) + with (part / f"received_reprocess_{logical_date.strftime('%Y%m%dT%H%M')}.csv").open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(missing) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(missing)}) + return len(missing) + + log_recheck = BashOperator( + task_id="log_recheck", + bash_command=( + "echo \"{{ params.service_date }} rechecked at {{ dag_run.run_after | ts }}: " + "{{ ti.xcom_pull(task_ids='recheck_partition') }} claim(s) recovered\" " + f">> {MARTS}/_RECHECK_LOG" + ), + ) + recheck_partition() >> log_recheck + + +claims_reprocess() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_service_date_marts.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_service_date_marts.py new file mode 100644 index 0000000..def85b1 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/claims_service_date_marts.py @@ -0,0 +1,80 @@ +"""Rebuilds the service-date marts for exactly the partitions that changed. + +Triggered by the ``claims-by-service-date`` alias. For each partition named by +the triggering events it rebuilds output/marts/claims_by_service_date/.csv +from all files of the partition, then records what it rebuilt (with the number +of new claims each partition received) in output/marts/rebuilds/, and appends +the partition list to output/marts/_READY_PARTITIONS for the BI refresh. +Rebuild records are numbered in run order. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_of + + +def rebuild_mart(day: str) -> None: + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # claims, billed, denied + for path in sorted((LAKE / "claims" / f"service_date={day}").glob("*.csv")): + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + s = stats[row["provider_id"]] + s[0] += 1 + s[1] += int(row["billed_cents"]) + s[2] += row["status"] == "denied" + out = MARTS / "claims_by_service_date" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["provider_id", "claims", "billed_cents", "denied"]) + for provider in sorted(stats): + writer.writerow([provider, *stats[provider]]) + + +@dag( + schedule=[CLAIMS_BY_SERVICE_DATE], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data"}, + tags=["claims", "marts"], +) +def claims_service_date_marts(): + @task + def rebuild_partitions(triggering_asset_events=None) -> list[str]: + # Airflow 3 lists each event under its asset AND under the alias it came through; + # read them once, through the alias. + new_claims: dict[str, int] = defaultdict(int) + for key in triggering_asset_events: + if not hasattr(key, "uri"): # the alias key repeats the same events + continue + new_claims[partition_of(key.uri)] += sum(e.extra.get("claims", 0) for e in triggering_asset_events[key]) + days = sorted(new_claims) + for day in days: + rebuild_mart(day) + records = MARTS / "rebuilds" + records.mkdir(parents=True, exist_ok=True) + record = records / f"{len(list(records.glob('*.json'))) + 1:04d}_{days[0]}_{days[-1]}.json" + record.write_text(json.dumps({"new_claims": new_claims}, indent=2, sort_keys=True) + "\n") + return days + + publish = BashOperator( + task_id="publish_ready", + bash_command=( + "echo \"{{ ti.xcom_pull(task_ids='rebuild_partitions') | join(',') }}\" " + f">> {MARTS}/_READY_PARTITIONS" + ), + ) + rebuild_partitions() >> publish + + +claims_service_date_marts() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/denials_digest_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/denials_digest_daily.py new file mode 100644 index 0000000..5cfa6a7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/denials_digest_daily.py @@ -0,0 +1,53 @@ +"""Daily digest of denied claims for the previous service date, by provider. + +Provider names and networks come from the latest provider-directory snapshot +taken at or before this run's date (its version is published in XCom by +provider_directory_daily). Output: output/digests/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE, MARTS, PROJECT_ROOT + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-ops"}, + tags=["claims", "reporting"], +) +def denials_digest_daily(): + @task + def build_digest(ds=None, ti=None) -> str: + # Airflow 3 matches cross-DAG XCom pulls on this run's run_id, so the directory + # DAG's values are not visible here; use the latest snapshot at or before ds. + snapshots = sorted(p.stem for p in (LAKE / "providers").glob("*.csv")) + version = [s for s in snapshots if s <= ds][-1] + with (LAKE / "providers" / f"{version}.csv").open(newline="") as fh: + directory = {r["provider_id"]: r for r in csv.DictReader(fh)} + mart = MARTS / "claims_by_service_date" / f"{ds}.csv" + rows = [] + if mart.exists(): + with mart.open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) if int(r["denied"]) > 0] + out = PROJECT_ROOT / "output" / "digests" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + fh.write(f"# directory version {version}\n") + writer = csv.writer(fh) + writer.writerow(["provider_id", "name", "network", "denied", "claims"]) + for r in rows: + p = directory.get(r["provider_id"], {}) + writer.writerow([r["provider_id"], p.get("name", "?"), p.get("network", "?"), r["denied"], r["claims"]]) + return ds + + build_digest() + + +denials_digest_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/provider_directory_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/provider_directory_daily.py new file mode 100644 index 0000000..23f4ef6 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/dags/provider_directory_daily.py @@ -0,0 +1,60 @@ +"""Daily provider-directory snapshot (effective for the previous day). + +Writes output/lake/providers/.csv plus .status, which says whether the +directory changed since the previous snapshot (network managers only review +changed days). The snapshot fingerprint is kept in XCom between runs. +""" + +from __future__ import annotations + +import csv +import hashlib + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import PROVIDERS +from claims_lib.hooks import ClaimsExportHook + +COLUMNS = ("provider_id", "name", "network") + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-data", "retries": 1}, + tags=["claims", "reference-data"], +) +def provider_directory_daily(): + @task(outlets=[PROVIDERS]) + def sync_providers(ds=None, ti=None) -> str: + rows = [r for r in ClaimsExportHook().read("providers_export.csv") if r["valid_from"] == ds] + out = LAKE / "providers" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=list(COLUMNS)) + writer.writeheader() + for r in rows: + writer.writerow({k: r[k] for k in COLUMNS}) + + fingerprint = hashlib.sha256( + "\n".join(",".join(r[k] for k in COLUMNS) for r in rows).encode()).hexdigest() + # previous fingerprint = hash of the latest earlier snapshot on disk + previous = None + earlier = sorted(p for p in (LAKE / "providers").glob("*.csv") if p.stem < ds) + if earlier: + with earlier[-1].open(newline="") as fh: + prev_rows = list(csv.DictReader(fh)) + previous = hashlib.sha256( + "\n".join(",".join(r[k] for k in COLUMNS) for r in prev_rows).encode()).hexdigest() + status = "first snapshot" if previous is None else ("unchanged" if previous == fingerprint else "changed") + (LAKE / "providers" / f"{ds}.status").write_text(status + "\n") + ti.xcom_push(key="fingerprint", value=fingerprint) + return ds # directory version consumed by denials_digest_daily + + sync_providers() + + +provider_directory_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/assets.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/assets.py new file mode 100644 index 0000000..1513ee9 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/assets.py @@ -0,0 +1,23 @@ +"""Datasets of the claims lake. + +Claims land in one partition per service date. The intake attaches each +partition it touched to the ``claims-by-service-date`` alias, so consumers do +not need to know the partitions in advance. +""" + +from __future__ import annotations + +from airflow.sdk import Asset, AssetAlias + +CLAIMS_BY_SERVICE_DATE = AssetAlias("claims-by-service-date") +PROVIDERS = Asset("lake://claims/providers") +PARTITION_PREFIX = "lake://claims/service_date=" + + +def partition_dataset(service_date: str) -> Asset: + return Asset(f"{PARTITION_PREFIX}{service_date}") + + +def partition_of(uri: str) -> str: + """Service date of a partition dataset URI.""" + return uri[len(PARTITION_PREFIX):] diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/hooks.py b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/hooks.py new file mode 100644 index 0000000..4c66951 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/plugins/claims_lib/hooks.py @@ -0,0 +1,29 @@ +"""Hook for the clearing-house export drop (connection ``claims_export``).""" + +from __future__ import annotations + +import csv +from pathlib import Path + +from airflow.sdk import BaseHook + + +class ClaimsExportHook(BaseHook): + """Reads the CSV exports from the directory in the connection's ``export_dir`` extra.""" + + conn_name_attr = "claims_export_conn_id" + default_conn_name = "claims_export" + conn_type = "fs" + hook_name = "Claims export drop" + + def __init__(self, claims_export_conn_id: str = default_conn_name) -> None: + super().__init__() + self.claims_export_conn_id = claims_export_conn_id + + def export_dir(self) -> Path: + conn = self.get_connection(self.claims_export_conn_id) + return Path(conn.extra_dejson["export_dir"]) + + def read(self, name: str) -> list[dict[str, str]]: + with (self.export_dir() / name).open(newline="") as fh: + return list(csv.DictReader(fh)) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/requirements.txt b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/requirements.txt new file mode 100644 index 0000000..5c62f9e --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/alt_reference/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-claims-partitions/case.yaml b/evals/airflow/cases/hard-migration-claims-partitions/case.yaml new file mode 100644 index 0000000..a20b38a --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/case.yaml @@ -0,0 +1,32 @@ +id: hard-migration-claims-partitions +area: migration +suite: hard +split: dev +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +prompt: | + Please migrate our claims project (DAGs in dags/, shared hook and dataset + definitions in plugins/) from Airflow 2.11 to Airflow 3.3 (the local venv + already has 3.3.2); production switches over next week. Claims ops and the + BI team rely on the outputs, so behaviour must not change: every scheduled + run, every alias-triggered mart rebuild and the reprocess runs from the + README runbook must write exactly the same files as they do on 2.11. +description: | + Hard whole-project Airflow 2.11 -> 3.3 migration (5 DAGs, custom hook on an + fs connection, DatasetAlias with dynamic partition datasets). Pain points + P7 and P3. Runtime-only traps on 3.3: (1) triggering_asset_events lists + every alias-delivered event twice (under its asset and under the alias), + and its keys are Asset/AssetAlias objects, so the 2.x loop + `for uri, events in triggering_dataset_events.items()` must be rewritten to + read each event once (e.g. through the alias, event.asset.uri); (2) the + digest's cross-DAG `xcom_pull(dag_id=..., include_prior_dates=True)` is + matched on the current run_id in 3.x and returns None for a DAG on another + schedule, so the directory version must come from elsewhere (latest + snapshot at or before ds); (3) the reprocess DAG, triggered from the CLI + with config, used `logical_date` as its cutoff and file stamp: on 3.x it is + None, the trigger time is dag_run.run_after; (4) bare cron/preset schedules + become trigger timetables. Primary: imports, task graphs, schedules (alias + condition), and a replay of scheduled runs, scheduler-style alias-triggered + runs and the CLI trigger with config under the Airflow 3 worker DB block, + with every output equal to the original replayed in 2.11. diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/.gitignore b/evals/airflow/cases/hard-migration-claims-partitions/fixture/.gitignore new file mode 100644 index 0000000..179b486 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/.gitignore @@ -0,0 +1,3 @@ +output/ +__pycache__/ +*.pyc diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/README.md b/evals/airflow/cases/hard-migration-claims-partitions/fixture/README.md new file mode 100644 index 0000000..5063754 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/README.md @@ -0,0 +1,30 @@ +# claims-pipelines + +Health-claims intake and service-date marts. Deployed with +`dags_folder=dags/` and `plugins_folder=plugins/` (locally: +`export AIRFLOW__CORE__PLUGINS_FOLDER=$PWD/plugins`). The clearing-house drop is +the `claims_export` connection (`fs` type, `{"export_dir": ...}` extra); +production defines it as +`AIRFLOW_CONN_CLAIMS_EXPORT='{"conn_type": "fs", "extra": {"export_dir": "/srv/claims/export"}}'` +(locally, point it at `data/`). Times are UTC. + +| DAG | Schedule | What it does | +|---|---|---| +| `claims_intake_hourly` | hourly | Appends the claims received during the hour to their service-date partition, `output/lake/claims/service_date=/`, and attaches each touched partition to the `claims-by-service-date` alias with its new-claim count | +| `claims_service_date_marts` | on the `claims-by-service-date` alias | Rebuilds `output/marts/claims_by_service_date/.csv` for exactly the partitions in the triggering events, writes a numbered rebuild record (new claims per partition) to `output/marts/rebuilds/`, and appends the partition list to `output/marts/_READY_PARTITIONS` | +| `provider_directory_daily` | 03:00 | Snapshots the previous day's provider directory to `output/lake/providers/.csv`, with `.status` saying whether it changed since the previous snapshot; its XCom is the directory version | +| `denials_digest_daily` | 07:00 | Denied claims of the previous service date per provider, named from the latest directory snapshot at or before that date: `output/digests/.csv` | +| `claims_reprocess` | manual | See runbook | + +## Ops runbook + +- **`claims_reprocess`**: from the ops box, + `airflow dags trigger claims_reprocess --conf '{"service_date": "YYYY-MM-DD"}'` + (no logical date). + It lands every claim of that service date received before the moment of + the trigger that the partition is missing (file + `received_reprocess_.csv`), the marts for + that date are rebuilt, and the recheck is logged with the trigger time in + `output/marts/_RECHECK_LOG`. + +`data/` stands in for the clearing-house export drop. diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_intake_hourly.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_intake_hourly.py new file mode 100644 index 0000000..a3d04a0 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_intake_hourly.py @@ -0,0 +1,54 @@ +"""Hourly intake of clearing-house claims into service-date partitions. + +Every claim received during the hour is appended to +output/lake/claims/service_date=/received_.csv, and every +partition touched is attached to the ``claims-by-service-date`` alias with the +number of claims it received. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.decorators import dag, task + +from claims_lib import LAKE +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule_interval="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data", "retries": 2}, + tags=["claims", "ingest"], +) +def claims_intake_hourly(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def land_claims(data_interval_start=None, data_interval_end=None, outlet_events=None) -> int: + lo = data_interval_start.strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + by_day: dict[str, list[dict]] = defaultdict(list) + for row in ClaimsExportHook().read("claims_export.csv"): + if lo <= row["received_at"] < hi: + by_day[row["service_date"]].append(row) + for day, rows in sorted(by_day.items()): + path = LAKE / "claims" / f"service_date={day}" / f"received_{data_interval_start.strftime('%Y%m%dT%H')}.csv" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(rows)}) + return sum(len(r) for r in by_day.values()) + + land_claims() + + +claims_intake_hourly() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_reprocess.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_reprocess.py new file mode 100644 index 0000000..4a4bceb --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_reprocess.py @@ -0,0 +1,66 @@ +"""On-demand re-check of one service-date partition (triggered by claims ops). + +Trigger with config ``{"service_date": "YYYY-MM-DD"}``. Re-reads the export +for every claim of that service date received before the trigger time, writes +any claim missing from the partition to received_reprocess_.csv, +attaches the partition to the alias so the marts are rebuilt, and logs the +recheck (with the trigger time) to output/marts/_RECHECK_LOG. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.decorators import dag, task +from airflow.models.param import Param +from airflow.operators.bash import BashOperator + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=None, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + params={"service_date": Param("2026-03-01", type="string", format="date")}, + default_args={"owner": "claims-ops"}, + tags=["claims", "ops"], +) +def claims_reprocess(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def recheck_partition(params=None, logical_date=None, outlet_events=None) -> int: + day = params["service_date"] + cutoff = logical_date.strftime("%Y-%m-%d %H:%M:%S") + part = LAKE / "claims" / f"service_date={day}" + landed = set() + for path in part.glob("*.csv"): + with path.open(newline="") as fh: + landed |= {r["claim_id"] for r in csv.DictReader(fh)} + missing = [r for r in ClaimsExportHook().read("claims_export.csv") + if r["service_date"] == day and r["received_at"] < cutoff and r["claim_id"] not in landed] + if missing: + part.mkdir(parents=True, exist_ok=True) + with (part / f"received_reprocess_{logical_date.strftime('%Y%m%dT%H%M')}.csv").open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(missing) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(missing)}) + return len(missing) + + log_recheck = BashOperator( + task_id="log_recheck", + bash_command=( + "echo \"{{ params.service_date }} rechecked at {{ ts }}: " + "{{ ti.xcom_pull(task_ids='recheck_partition') }} claim(s) recovered\" " + f">> {MARTS}/_RECHECK_LOG" + ), + ) + recheck_partition() >> log_recheck + + +claims_reprocess() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_service_date_marts.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_service_date_marts.py new file mode 100644 index 0000000..6cecf88 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/claims_service_date_marts.py @@ -0,0 +1,76 @@ +"""Rebuilds the service-date marts for exactly the partitions that changed. + +Triggered by the ``claims-by-service-date`` alias. For each partition named by +the triggering events it rebuilds output/marts/claims_by_service_date/.csv +from all files of the partition, then records what it rebuilt (with the number +of new claims each partition received) in output/marts/rebuilds/, and appends +the partition list to output/marts/_READY_PARTITIONS for the BI refresh. +Rebuild records are numbered in run order. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.decorators import dag, task +from airflow.operators.bash import BashOperator + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_of + + +def rebuild_mart(day: str) -> None: + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # claims, billed, denied + for path in sorted((LAKE / "claims" / f"service_date={day}").glob("*.csv")): + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + s = stats[row["provider_id"]] + s[0] += 1 + s[1] += int(row["billed_cents"]) + s[2] += row["status"] == "denied" + out = MARTS / "claims_by_service_date" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["provider_id", "claims", "billed_cents", "denied"]) + for provider in sorted(stats): + writer.writerow([provider, *stats[provider]]) + + +@dag( + schedule=[CLAIMS_BY_SERVICE_DATE], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data"}, + tags=["claims", "marts"], +) +def claims_service_date_marts(): + @task + def rebuild_partitions(triggering_dataset_events=None) -> list[str]: + new_claims: dict[str, int] = defaultdict(int) + for uri, events in triggering_dataset_events.items(): + new_claims[partition_of(uri)] += sum(event.extra.get("claims", 0) for event in events) + days = sorted(new_claims) + for day in days: + rebuild_mart(day) + records = MARTS / "rebuilds" + records.mkdir(parents=True, exist_ok=True) + record = records / f"{len(list(records.glob('*.json'))) + 1:04d}_{days[0]}_{days[-1]}.json" + record.write_text(json.dumps({"new_claims": new_claims}, indent=2, sort_keys=True) + "\n") + return days + + publish = BashOperator( + task_id="publish_ready", + bash_command=( + "echo \"{{ ti.xcom_pull(task_ids='rebuild_partitions') | join(',') }}\" " + f">> {MARTS}/_READY_PARTITIONS" + ), + ) + rebuild_partitions() >> publish + + +claims_service_date_marts() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/denials_digest_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/denials_digest_daily.py new file mode 100644 index 0000000..2d92a6a --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/denials_digest_daily.py @@ -0,0 +1,50 @@ +"""Daily digest of denied claims for the previous service date, by provider. + +Provider names and networks come from the latest provider-directory snapshot +taken at or before this run's date (its version is published in XCom by +provider_directory_daily). Output: output/digests/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.decorators import dag, task + +from claims_lib import LAKE, MARTS, PROJECT_ROOT + + +@dag( + schedule_interval="0 7 * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-ops"}, + tags=["claims", "reporting"], +) +def denials_digest_daily(): + @task + def build_digest(ds=None, ti=None) -> str: + version = ti.xcom_pull(dag_id="provider_directory_daily", task_ids="sync_providers", include_prior_dates=True) + with (LAKE / "providers" / f"{version}.csv").open(newline="") as fh: + directory = {r["provider_id"]: r for r in csv.DictReader(fh)} + mart = MARTS / "claims_by_service_date" / f"{ds}.csv" + rows = [] + if mart.exists(): + with mart.open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) if int(r["denied"]) > 0] + out = PROJECT_ROOT / "output" / "digests" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + fh.write(f"# directory version {version}\n") + writer = csv.writer(fh) + writer.writerow(["provider_id", "name", "network", "denied", "claims"]) + for r in rows: + p = directory.get(r["provider_id"], {}) + writer.writerow([r["provider_id"], p.get("name", "?"), p.get("network", "?"), r["denied"], r["claims"]]) + return ds + + build_digest() + + +denials_digest_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/provider_directory_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/provider_directory_daily.py new file mode 100644 index 0000000..533c4b1 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/dags/provider_directory_daily.py @@ -0,0 +1,53 @@ +"""Daily provider-directory snapshot (effective for the previous day). + +Writes output/lake/providers/.csv plus .status, which says whether the +directory changed since the previous snapshot (network managers only review +changed days). The snapshot fingerprint is kept in XCom between runs. +""" + +from __future__ import annotations + +import csv +import hashlib + +import pendulum +from airflow.decorators import dag, task + +from claims_lib import LAKE +from claims_lib.assets import PROVIDERS +from claims_lib.hooks import ClaimsExportHook + +COLUMNS = ("provider_id", "name", "network") + + +@dag( + schedule_interval="0 3 * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-data", "retries": 1}, + tags=["claims", "reference-data"], +) +def provider_directory_daily(): + @task(outlets=[PROVIDERS]) + def sync_providers(ds=None, ti=None) -> str: + rows = [r for r in ClaimsExportHook().read("providers_export.csv") if r["valid_from"] == ds] + out = LAKE / "providers" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=list(COLUMNS)) + writer.writeheader() + for r in rows: + writer.writerow({k: r[k] for k in COLUMNS}) + + fingerprint = hashlib.sha256( + "\n".join(",".join(r[k] for k in COLUMNS) for r in rows).encode()).hexdigest() + previous = ti.xcom_pull(task_ids="sync_providers", key="fingerprint", include_prior_dates=True) + status = "first snapshot" if previous is None else ("unchanged" if previous == fingerprint else "changed") + (LAKE / "providers" / f"{ds}.status").write_text(status + "\n") + ti.xcom_push(key="fingerprint", value=fingerprint) + return ds # directory version consumed by denials_digest_daily + + sync_providers() + + +provider_directory_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/claims_export.csv b/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/claims_export.csv new file mode 100644 index 0000000..5523ffc --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/claims_export.csv @@ -0,0 +1,65 @@ +claim_id,provider_id,service_date,billed_cents,status,received_at +C00000,PRV-11,2026-03-04,1500,approved,2026-03-04 20:07:00 +C00001,PRV-14,2026-03-02,2873,approved,2026-03-04 20:30:00 +C00002,PRV-13,2026-03-03,4246,approved,2026-03-04 21:00:00 +C00003,PRV-12,2026-03-04,5619,denied,2026-03-04 21:37:00 +C00004,PRV-11,2026-03-02,6992,approved,2026-03-04 22:02:00 +C00005,PRV-14,2026-03-03,8365,approved,2026-03-04 22:34:00 +C00006,PRV-13,2026-03-04,9738,approved,2026-03-04 23:13:00 +C00007,PRV-12,2026-03-02,11111,approved,2026-03-04 23:40:00 +C00008,PRV-11,2026-03-03,12484,approved,2026-03-05 00:14:00 +C00009,PRV-14,2026-03-04,13857,approved,2026-03-05 00:55:00 +C00010,PRV-13,2026-03-02,15230,denied,2026-03-05 01:24:00 +C00011,PRV-12,2026-03-03,16603,approved,2026-03-05 02:00:00 +C00012,PRV-11,2026-03-04,17976,approved,2026-03-05 02:24:00 +C00013,PRV-14,2026-03-02,19349,approved,2026-03-05 02:55:00 +C00014,PRV-13,2026-03-04,20722,approved,2026-03-05 03:33:00 +C00015,PRV-12,2026-03-05,22095,approved,2026-03-05 03:59:00 +C00016,PRV-11,2026-03-03,23468,approved,2026-03-05 04:32:00 +C00017,PRV-14,2026-03-04,24841,denied,2026-03-05 05:12:00 +C00018,PRV-13,2026-03-05,26214,approved,2026-03-05 05:40:00 +C00019,PRV-12,2026-03-03,27587,approved,2026-03-05 06:15:00 +C00020,PRV-11,2026-03-04,28960,approved,2026-03-05 06:38:00 +C00021,PRV-14,2026-03-05,30333,approved,2026-03-05 07:08:00 +C00022,PRV-13,2026-03-03,31706,approved,2026-03-05 07:45:00 +C00023,PRV-12,2026-03-04,33079,approved,2026-03-05 08:10:00 +C00024,PRV-11,2026-03-05,34452,denied,2026-03-05 08:42:00 +C00025,PRV-14,2026-03-03,35825,approved,2026-03-05 09:21:00 +C00026,PRV-13,2026-03-04,37198,approved,2026-03-05 09:48:00 +C00027,PRV-12,2026-03-05,38571,approved,2026-03-05 10:22:00 +C00028,PRV-11,2026-03-03,39944,approved,2026-03-05 11:03:00 +C00029,PRV-14,2026-03-04,41317,approved,2026-03-05 11:32:00 +C00030,PRV-13,2026-03-05,42690,approved,2026-03-05 12:08:00 +C00031,PRV-12,2026-03-03,44063,denied,2026-03-05 12:32:00 +C00032,PRV-11,2026-03-04,45436,approved,2026-03-05 13:03:00 +C00033,PRV-14,2026-03-05,46809,approved,2026-03-05 13:41:00 +C00034,PRV-13,2026-03-03,48182,approved,2026-03-05 14:07:00 +C00035,PRV-12,2026-03-04,49555,approved,2026-03-05 14:40:00 +C00036,PRV-11,2026-03-05,50928,approved,2026-03-05 15:20:00 +C00037,PRV-14,2026-03-03,52301,approved,2026-03-05 15:48:00 +C00038,PRV-13,2026-03-04,53674,denied,2026-03-05 16:23:00 +C00039,PRV-12,2026-03-05,55047,approved,2026-03-05 16:46:00 +C00040,PRV-11,2026-03-03,56420,approved,2026-03-05 17:16:00 +C00041,PRV-14,2026-03-04,57793,approved,2026-03-05 17:53:00 +C00042,PRV-13,2026-03-05,59166,approved,2026-03-05 18:18:00 +C00043,PRV-12,2026-03-03,60539,approved,2026-03-05 18:50:00 +C00044,PRV-11,2026-03-04,61912,approved,2026-03-05 19:29:00 +C00045,PRV-14,2026-03-05,63285,denied,2026-03-05 19:56:00 +C00046,PRV-13,2026-03-03,64658,approved,2026-03-05 20:30:00 +C00047,PRV-12,2026-03-04,66031,approved,2026-03-05 21:11:00 +C00048,PRV-11,2026-03-05,67404,approved,2026-03-05 21:40:00 +C00049,PRV-14,2026-03-03,68777,approved,2026-03-05 22:16:00 +C00050,PRV-13,2026-03-04,70150,approved,2026-03-05 22:40:00 +C00051,PRV-12,2026-03-05,71523,approved,2026-03-05 23:11:00 +C00052,PRV-11,2026-03-03,72896,denied,2026-03-05 23:49:00 +C00053,PRV-14,2026-03-04,74269,approved,2026-03-06 00:15:00 +C00054,PRV-13,2026-03-05,75642,approved,2026-03-06 00:48:00 +C00055,PRV-12,2026-03-03,77015,approved,2026-03-06 01:28:00 +C00056,PRV-11,2026-03-04,78388,approved,2026-03-06 01:56:00 +C00057,PRV-14,2026-03-05,79761,approved,2026-03-06 02:31:00 +C00058,PRV-13,2026-03-03,81134,approved,2026-03-06 02:54:00 +C00059,PRV-12,2026-03-05,82507,denied,2026-03-06 03:24:00 +C00060,PRV-11,2026-03-06,83880,approved,2026-03-06 04:01:00 +C00061,PRV-14,2026-03-04,85253,approved,2026-03-06 04:26:00 +C00062,PRV-13,2026-03-05,86626,approved,2026-03-06 04:58:00 +C00063,PRV-12,2026-03-06,87999,approved,2026-03-06 05:37:00 diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/providers_export.csv b/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/providers_export.csv new file mode 100644 index 0000000..a88dcb1 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/data/providers_export.csv @@ -0,0 +1,29 @@ +valid_from,provider_id,name,network +2026-03-01,PRV-11,Clinic 11,in +2026-03-01,PRV-12,Clinic 12,in +2026-03-01,PRV-13,Clinic 13,in +2026-03-01,PRV-14,Clinic 14,out +2026-03-02,PRV-11,Clinic 11,in +2026-03-02,PRV-12,Clinic 12,in +2026-03-02,PRV-13,Clinic 13,out +2026-03-02,PRV-14,Clinic 14,in +2026-03-03,PRV-11,Clinic 11,in +2026-03-03,PRV-12,Clinic 12,out +2026-03-03,PRV-13,Clinic 13,in +2026-03-03,PRV-14,Clinic 14,in +2026-03-04,PRV-11,Clinic 11,out +2026-03-04,PRV-12,Clinic 12,in +2026-03-04,PRV-13,Clinic 13,in +2026-03-04,PRV-14,Clinic 14,in +2026-03-05,PRV-11,Clinic 11,out +2026-03-05,PRV-12,Clinic 12,in +2026-03-05,PRV-13,Clinic 13,in +2026-03-05,PRV-14,Clinic 14,in +2026-03-06,PRV-11,Clinic 11,out +2026-03-06,PRV-12,Clinic 12,in +2026-03-06,PRV-13,Clinic 13,in +2026-03-06,PRV-14,Clinic 14,in +2026-03-07,PRV-11,Clinic 11,out +2026-03-07,PRV-12,Clinic 12,in +2026-03-07,PRV-13,Clinic 13,in +2026-03-07,PRV-14,Clinic 14,in diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/__init__.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/__init__.py new file mode 100644 index 0000000..a96afa0 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/__init__.py @@ -0,0 +1,7 @@ +"""Shared code for the claims DAGs (on sys.path via the plugins folder).""" + +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +LAKE = PROJECT_ROOT / "output" / "lake" +MARTS = PROJECT_ROOT / "output" / "marts" diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/assets.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/assets.py new file mode 100644 index 0000000..728dc64 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/assets.py @@ -0,0 +1,23 @@ +"""Datasets of the claims lake. + +Claims land in one partition per service date. The intake attaches each +partition it touched to the ``claims-by-service-date`` alias, so consumers do +not need to know the partitions in advance. +""" + +from __future__ import annotations + +from airflow.datasets import Dataset, DatasetAlias + +CLAIMS_BY_SERVICE_DATE = DatasetAlias("claims-by-service-date") +PROVIDERS = Dataset("lake://claims/providers") +PARTITION_PREFIX = "lake://claims/service_date=" + + +def partition_dataset(service_date: str) -> Dataset: + return Dataset(f"{PARTITION_PREFIX}{service_date}") + + +def partition_of(uri: str) -> str: + """Service date of a partition dataset URI.""" + return uri[len(PARTITION_PREFIX):] diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/hooks.py b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/hooks.py new file mode 100644 index 0000000..02b963c --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/plugins/claims_lib/hooks.py @@ -0,0 +1,29 @@ +"""Hook for the clearing-house export drop (connection ``claims_export``).""" + +from __future__ import annotations + +import csv +from pathlib import Path + +from airflow.hooks.base import BaseHook + + +class ClaimsExportHook(BaseHook): + """Reads the CSV exports from the directory in the connection's ``export_dir`` extra.""" + + conn_name_attr = "claims_export_conn_id" + default_conn_name = "claims_export" + conn_type = "fs" + hook_name = "Claims export drop" + + def __init__(self, claims_export_conn_id: str = default_conn_name) -> None: + super().__init__() + self.claims_export_conn_id = claims_export_conn_id + + def export_dir(self) -> Path: + conn = self.get_connection(self.claims_export_conn_id) + return Path(conn.extra_dejson["export_dir"]) + + def read(self, name: str) -> list[dict[str, str]]: + with (self.export_dir() / name).open(newline="") as fh: + return list(csv.DictReader(fh)) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/fixture/requirements.txt b/evals/airflow/cases/hard-migration-claims-partitions/fixture/requirements.txt new file mode 100644 index 0000000..7056ae9 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==2.11.2 +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-claims-partitions/grade.py b/evals/airflow/cases/hard-migration-claims-partitions/grade.py new file mode 100644 index 0000000..4fab686 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/grade.py @@ -0,0 +1,65 @@ +"""Grader for hard-migration-claims-partitions. + +The fixture is an Airflow 2.11 claims project: an hourly intake that attaches +the service-date partitions it touched to a DatasetAlias (custom hook on an +``fs`` connection), a consumer scheduled on the alias that rebuilds exactly the +partitions named by ``triggering_dataset_events`` (keyed by dataset URI), a +directory snapshot whose version a digest reads with a cross-DAG +``xcom_pull(include_prior_dates=True)``, and a manual-only reprocess DAG +triggered with config that uses the run's ``logical_date`` as its cutoff. + +The original is replayed in the 2.11 env and the agent's project in the 3.3 env +(task code under the Airflow 3 worker's metadata-DB block): scheduled runs, +scheduler-style alias-triggered runs and a CLI trigger with config. Every file +under ``output/`` must match. See ``hardsim``. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import hardsim as hs # noqa: E402 + +DAG_SHAPES = { + "claims_intake_hourly": ({"land_claims"}, []), + "claims_service_date_marts": ({"rebuild_partitions", "publish_ready"}, [["rebuild_partitions", "publish_ready"]]), + "provider_directory_daily": ({"sync_providers"}, []), + "denials_digest_daily": ({"build_digest"}, []), + "claims_reprocess": ({"recheck_partition", "log_recheck"}, [["recheck_partition", "log_recheck"]]), +} +STEPS = [ + ["provider_directory_daily", "2026-03-04T03:00:00+00:00"], + ["provider_directory_daily", "2026-03-05T03:00:00+00:00"], + ["claims_intake_hourly", "2026-03-04T21:00:00+00:00"], + ["claims_intake_hourly", "2026-03-04T22:00:00+00:00"], + ["claims_intake_hourly", "2026-03-04T23:00:00+00:00"], + ["claims_service_date_marts", hs.asset_trigger("2026-03-04T23:05:00+00:00")], + ["claims_intake_hourly", "2026-03-05T00:00:00+00:00"], + ["claims_intake_hourly", "2026-03-05T01:00:00+00:00"], + ["claims_service_date_marts", hs.asset_trigger("2026-03-05T01:05:00+00:00")], + ["denials_digest_daily", "2026-03-05T07:00:00+00:00"], + ["provider_directory_daily", "2026-03-06T03:00:00+00:00"], + ["denials_digest_daily", "2026-03-06T07:00:00+00:00"], + ["claims_reprocess", hs.trigger("2026-03-06T09:00:00+00:00", {"service_date": "2026-03-04"})], + ["claims_service_date_marts", hs.asset_trigger("2026-03-06T09:05:00+00:00")], + # the 03-07 directory sync has not run (late export): the digest falls back to the latest snapshot + ["denials_digest_daily", "2026-03-07T07:00:00+00:00"], +] +AREAS = [ + ("hourly intake lands the same partition files", ("lake/claims/",)), + ("partition marts, rebuild records and the READY list match", ("marts/",)), + ("provider directory snapshots match", ("lake/providers/",)), + ("denials digests use the same directory versions and rows", ("digests/",)), +] + + +def env_extra(ws: Path) -> dict: + conn = '{"conn_type": "fs", "extra": {"export_dir": "%s"}}' % (ws / "data") + return {"AIRFLOW__CORE__PLUGINS_FOLDER": str(ws / "plugins"), "AIRFLOW_CONN_CLAIMS_EXPORT": conn} + + +if __name__ == "__main__": + hs.grade_project(CASE_DIR, dag_shapes=DAG_SHAPES, steps=STEPS, areas=AREAS, env_extra=env_extra) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/hardsim.py b/evals/airflow/cases/hard-migration-claims-partitions/hardsim.py new file mode 100644 index 0000000..bd0713b --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/hardsim.py @@ -0,0 +1,658 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +Extended for the ``hard-migration-*`` cases (identical in each of them): CLI/API +triggers without a logical date (``trigger``), scheduler-style asset/dataset +triggered runs that consume the recorded events (``asset_trigger``), and +multi-DAG replays against one metadata DB in one process (``replay``). +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + # 2.x dag.test() does not write DAGs to the DB; dataset references + # (needed to record and consume dataset events) come from this sync. + if not bag.import_errors: + bag.sync_to_db() + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +def _run_state(dr_id, session_factory): + from airflow.models.dagrun import DagRun + with session_factory() as s: + dr = s.get(DagRun, dr_id) + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + + +def run_asset_triggered(dag, run_after, row): + """Run ``dag`` the way the scheduler runs an asset/dataset-triggered DagRun at ``run_after``. + + Consumed events: every event of an asset the DAG is scheduled on that no earlier + asset-triggered run of this DAG consumed (the grader triggers only once the DAG's + condition is met). 3.x: logical_date and data_interval are None (scheduler code). + 2.x: execution_date = run_after and the interval comes from + ``timetable.data_interval_for_events`` (scheduler code).""" + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + created = {} + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.models.asset import AssetEvent, DagScheduleAssetReference + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, session, **_): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.ASSET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_asset_events} + from sqlalchemy import or_ + from airflow.models.asset import AssetAliasModel, DagScheduleAssetAliasReference + asset_ids = [r.asset_id for r in session.query(DagScheduleAssetReference) + .filter(DagScheduleAssetReference.dag_id == dag.dag_id).all()] + via_alias = AssetEvent.source_aliases.any(AssetAliasModel.scheduled_dags.any( + DagScheduleAssetAliasReference.dag_id == dag.dag_id)) + events = [e for e in session.query(AssetEvent).filter(or_(AssetEvent.asset_id.in_(asset_ids), via_alias)) + .order_by(AssetEvent.timestamp, AssetEvent.id).all() if e.id not in consumed] + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=DagRunType.ASSET_TRIGGERED, logical_date=None, + run_after=run_after), + logical_date=None, data_interval=None, run_after=run_after, + run_type=DagRunType.ASSET_TRIGGERED, triggered_by=DagRunTriggeredByType.ASSET, + state=DagRunState.RUNNING, start_date=run_after, session=session, conf=None) + dr.consumed_asset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + drmod.get_or_create_dagrun = patched + try: + dag.test(logical_date=None, run_after=run_after) + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + from airflow.models.dataset import DagScheduleDatasetReference, DatasetEvent + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.DATASET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_dataset_events} + ds_ids = [r.dataset_id for r in session.query(DagScheduleDatasetReference) + .filter(DagScheduleDatasetReference.dag_id == dag.dag_id).all()] + events = [e for e in session.query(DatasetEvent).filter(DatasetEvent.dataset_id.in_(ds_ids)) + .order_by(DatasetEvent.timestamp, DatasetEvent.id).all() if e.id not in consumed] + interval = dag.timetable.data_interval_for_events(execution_date, events) + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(DagRunType.DATASET_TRIGGERED, execution_date), + run_type=DagRunType.DATASET_TRIGGERED, execution_date=execution_date, + data_interval=interval, state=DagRunState.RUNNING, start_date=execution_date, + session=session, external_trigger=False) + dr.consumed_dataset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + dagmod._get_or_create_dagrun = patched + try: + dag.test(execution_date=run_after) + finally: + dagmod._get_or_create_dagrun = orig + row["events"] = created.get("events") + if "id" not in created: + row["error"] = "asset-triggered DagRun was not created" + return None + row["state"], row["tasks"] = _run_state(created["id"], create_session) + return created["id"] + + +import contextlib + + +@contextlib.contextmanager +def run_kind(kind): + """Make dag.test() create the DagRun the scheduler (kind="scheduled") or a CLI + trigger (kind="trigger") would create: run_type, run_id format, triggered_by + (3.x) / external_trigger (2.x). dag.test() alone makes every run MANUAL.""" + from airflow.models.dagrun import DagRun + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + rtype = DagRunType.SCHEDULED if kind == "scheduled" else DagRunType.MANUAL + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, run_id, logical_date, data_interval, run_after, conf, session, start_date, **_): + from sqlalchemy import select + if logical_date is not None: + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.logical_date == logical_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=rtype, logical_date=logical_date, run_after=run_after), + logical_date=logical_date, data_interval=data_interval, run_after=run_after, conf=conf, + run_type=rtype, + triggered_by=DagRunTriggeredByType.TIMETABLE if kind == "scheduled" else DagRunTriggeredByType.CLI, + state=DagRunState.RUNNING, start_date=start_date or run_after, session=session) + + drmod.get_or_create_dagrun = patched + try: + yield + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + from sqlalchemy import select + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.execution_date == execution_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(rtype, execution_date), run_type=rtype, + execution_date=execution_date, data_interval=data_interval, state=DagRunState.RUNNING, + start_date=start_date or execution_date, session=session, conf=conf, + external_trigger=(kind == "trigger")) + + dagmod._get_or_create_dagrun = patched + try: + yield + finally: + dagmod._get_or_create_dagrun = orig + + +_DAGS = {} + + +def get_dag_tt(dag_id): + if dag_id not in _DAGS: + dag = load_dag(dag_id) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + _DAGS[dag_id] = (dag, core_timetable(dag)) + return _DAGS[dag_id] + + +def run_step(dag_id, spec): + dag, tt = get_dag_tt(dag_id) + RESULT.setdefault("timetables", {})[dag_id] = type(tt).__name__ + if isinstance(spec, dict) and "asset_trigger" in spec: + run_after = pendulum.parse(spec["asset_trigger"]) + row = {"dag_id": dag_id, "asset_trigger": spec["asset_trigger"]} + RESULT["runs"].append(row) + try: + run_asset_triggered(dag, run_after, row) + except Exception as exc: # noqa: BLE001 + import traceback + row["error"] = f"asset-triggered run raised {type(exc).__name__}: {exc}\n" + traceback.format_exc(limit=4) + return + if isinstance(spec, dict) and "trigger" in spec: + # CLI/API trigger (no logical date) at wall-clock T. 3.x: no logical date, run_after=T. + # 2.x: logical date = T (the trigger time), interval inferred from T. + run_after = pendulum.parse(spec["trigger"]) + row = {"dag_id": dag_id, "trigger": spec["trigger"]} + RESULT["runs"].append(row) + try: + with run_kind("trigger"): + if MAJOR >= 3: + dag.test(logical_date=None, run_after=run_after, run_conf=spec.get("conf")) + else: + dag.test(execution_date=run_after, run_conf=spec.get("conf")) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + if MAJOR >= 3: + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + with create_session() as s: + drs = s.query(DagRun).filter(DagRun.dag_id == dag_id, DagRun.run_after == run_after, + DagRun.logical_date.is_(None)).all() + if len(drs) == 1: + row["state"] = str(getattr(drs[0].state, "value", drs[0].state)) + row["tasks"] = {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in drs[0].get_task_instances(session=s)} + else: + row["state"], row["tasks"] = None, {} + row.setdefault("error", f"found {len(drs)} runs with run_after={run_after}") + else: + row["state"], row["tasks"] = task_states(dag_id, run_after) + forgive_deadline_bug(row) + return + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"dag_id": dag_id, "manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(dag_id, logical) + forgive_deadline_bug(row) + return + run_after = spec + row = {"dag_id": dag_id, "run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {dag_id} fires at {run_after} ({type(tt).__name__})" + return + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + with run_kind("scheduled"): + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(dag_id, info.logical_date) + forgive_deadline_bug(row) + + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + for dag_id, spec in P["steps"]: + try: + run_step(dag_id, spec) + except Exception as exc: # noqa: BLE001 (e.g. DAG missing): record, keep going + import traceback + RESULT["runs"].append({"dag_id": dag_id, "spec": spec, + "error": traceback.format_exc(limit=4)[-1500:]}) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is an ISO datetime T (the *scheduled* run that fires at T, with + the logical date and data interval the DAG's timetable gives it), ``trigger(T)`` (a + CLI/API trigger without a logical date at T), ``asset_trigger(T)`` (the scheduler's asset/dataset-triggered + run at T) or ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + return replay(env_py, workspace, [[dag_id, r] for r in runs], env=env, block_orm=block_orm, + timeout=timeout) + + +def replay(env_py: str, workspace: Path, steps: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 900) -> tuple[dict, str]: + """Like :func:`simulate`, for ``steps = [[dag_id, run_spec], ...]`` across DAGs, in one process.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), + "steps": [list(s) for s in steps], "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def asset_trigger(run_after: str) -> dict: + """Run spec: the asset/dataset-triggered run the scheduler creates at ``run_after``, + consuming every event of the DAG's assets not consumed by an earlier such run.""" + return {"asset_trigger": run_after} + + +def trigger(run_after: str, conf: dict | None = None) -> dict: + """Run spec: a CLI/API trigger (no logical date) at wall-clock ``run_after`` with no logical date + (3.x: ``dag.test(logical_date=None, run_after=T)``; 2.x: logical date = T), + optionally "with config" (``dag_run.conf``).""" + return {"trigger": run_after, "conf": conf} + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + def label(r): + for key, name in (("run_after", "scheduled"), ("trigger", "CLI trigger"), + ("asset_trigger", "asset-triggered"), ("manual", "dags test")): + if r.get(key): + return f"{r.get('dag_id', '')} {name} {r[key]}".strip() + return f"{r.get('dag_id', '')} {r.get('spec', '?')}" + return "; ".join( + f"{label(r)}: " + (str(r.get("error"))[-1200:] if r.get("error") else + f"state={r.get('state')} failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log + + +# --------------------------------------------------------------------------- +# Whole-project comparison grading shared by the hard-migration-* cases. + +_SCHEDULE_PROBE = r""" +from airflow.timetables.base import TimeRestriction + +def cond(obj): + if "Alias" in type(obj).__name__: # 2.x: _DatasetAliasCondition (an "any" of resolved datasets) + return "alias:" + obj.name + if hasattr(obj, "objects"): + kind = "all" if "All" in type(obj).__name__ else "any" + parts = [] + for o in obj.objects: # flatten nested groups of the same kind + c = cond(o) + parts.extend(c[kind] if isinstance(c, dict) and kind in c else [c]) + if len(parts) == 1: # [x], x, any(x) and all(x) are the same condition + return parts[0] + return {kind: sorted(parts, key=repr)} + return getattr(obj, "uri", None) or repr(obj) + +out = {} +for dag_id in DAG_IDS: + try: + dag = get_dag(dag_id) + except Exception as exc: + out[dag_id] = {"error": repr(exc)[:300]} + continue + tt = core_timetable(dag) + c = None + if type(tt).__name__ in ("AssetTriggeredTimetable", "DatasetTriggeredTimetable"): + c = getattr(tt, "asset_condition", None) or getattr(tt, "dataset_condition", None) + row = {"catchup": bool(dag.catchup), "timetable_kind": "assets" if c is not None else "time"} + if c is not None: + row["condition"] = cond(c) + else: + last, runs = None, [] + r = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + for _ in range(N_RUNS): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=r) + if info is None: + break + runs.append([info.run_after.isoformat(), info.data_interval.start.isoformat(), + info.data_interval.end.isoformat()]) + last = info.data_interval + row["first_runs"] = runs + out[dag_id] = row +RESULT = out +""" + + +def schedules(env_py: str, workspace: Path, dag_ids, env: dict, n_runs: int = 6) -> tuple[dict | None, str]: + """Per DAG: catchup, and either the asset condition or the first ``n_runs`` scheduled + (run_after, interval start, interval end) from start_date (catchup restriction).""" + code = f"DAG_IDS = {sorted(dag_ids)!r}\nN_RUNS = {n_runs}\n" + _SCHEDULE_PROBE + g.ensure_db(env_py, env) # 2.x resolves dataset aliases against the DB while parsing + res, proc = g.probe_json(env_py, code, workspace, env=env) + return res, proc.tail(20) + + +def _diff_detail(exp: dict[str, str], have: dict[str, str]) -> str: + if exp == have: + return "" + missing = sorted(set(exp) - set(have)) + extra = sorted(set(have) - set(exp)) + changed = sorted(k for k in set(exp) & set(have) if exp[k] != have[k]) + detail = f"missing={missing} unexpected={extra} changed={changed}" + for k in changed[:2]: + detail += f"\n--- {k} expected:\n{exp[k][:700]}\n--- got:\n{have[k][:700]}" + return detail + + +def grade_project(case_dir: Path, *, dag_shapes: dict, steps: list, areas: list, env_extra, + n_schedule_runs: int = 6) -> None: + """Standard grader: original replayed in 2.11 vs the agent's project replayed in 3.3. + + ``env_extra(ws) -> dict`` adds deployment settings (plugins folder, custom config) + for both runs. ``areas = [(check name, (output path prefixes...)), ...]``.""" + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + def env_for(env_py, root): + return g.airflow_env(root, env_py=env_py, extra=env_extra(Path(root))) + + orig_ws = original_workspace(case_dir) + py2 = g.env_python("2.11") + exp_sched, log = schedules(py2, orig_ws, dag_shapes, env_for(py2, orig_ws), n_schedule_runs) + if not exp_sched or any("error" in v for v in exp_sched.values()): + raise SystemExit(f"case bug: schedule probe failed on the original: {exp_sched}\n{log}") + shutil.rmtree(orig_ws / "output", ignore_errors=True) + orig, log = replay(py2, orig_ws, steps, env=env_for(py2, orig_ws), timeout=1500) + if not runs_ok(orig): + raise SystemExit(f"case bug: original failed in 2.11: {describe(orig)}\n{log}") + expected = snapshot(orig_ws) + + imp = g.import_dags(py, ws, env=env_for(py, ws)) + dags = imp["dags"] + grader.primary(f"all {len(dag_shapes)} DAGs import on Airflow 3.3 (dags/ + plugins/)", + imp["ok"] and not imp["import_errors"] and set(dag_shapes) <= set(dags), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"missing: {sorted(set(dag_shapes) - set(dags))}") + def keeps_shape(d, tasks, deps): + # Original task ids and ordering are kept; added helper tasks are allowed as long + # as every original dependency still holds (downstream reachable from upstream). + if d not in dags or not set(tasks) <= set(dags[d]["tasks"]): + return False + down: dict[str, set] = {} + for up, dn in dags[d]["deps"]: + down.setdefault(up, set()).add(dn) + + def reach(a, b): + seen, todo = set(), [a] + while todo: + n = todo.pop() + if n == b: + return True + if n not in seen: + seen.add(n) + todo.extend(down.get(n, ())) + return False + return all(reach(up, dn) for up, dn in deps) + + shape_ok = all(keeps_shape(d, t, deps) for d, (t, deps) in dag_shapes.items()) + grader.primary("every DAG keeps its tasks and dependencies (added helper tasks allowed)", shape_ok, + "; ".join(f"{d}: tasks={dags[d]['tasks']} deps={dags[d]['deps']}" if d in dags + else f"{d}: missing" for d in dag_shapes)) + + got_sched, log = schedules(py, ws, dag_shapes, env_for(py, ws), n_schedule_runs) + diffs = [f"{d}: expected {exp_sched[d]}\n got {(got_sched or {}).get(d)}" + for d in sorted(dag_shapes) if (got_sched or {}).get(d) != exp_sched[d]] + grader.primary("schedules unchanged: same scheduled runs and data intervals, catchup, asset conditions", + got_sched is not None and not diffs, "\n".join(diffs) or ("" if got_sched else log)) + + shutil.rmtree(ws / "output", ignore_errors=True) + got, log = replay(py, ws, steps, env=env_for(py, ws), block_orm=True, timeout=1500) + ok = runs_ok(got) + grader.primary("every replayed run (scheduled, asset-triggered, CLI-triggered) succeeds on 3.3", ok, + describe(got) + ("" if ok else "\n" + log[-2500:])) + snap = snapshot(ws) + for name, prefixes in areas: + exp = {k: v for k, v in expected.items() if k.startswith(prefixes)} + have = {k: v for k, v in snap.items() if k.startswith(prefixes)} + grader.primary(name, exp == have, _diff_detail(exp, have)) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_version_is_ds/dags/denials_digest_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_version_is_ds/dags/denials_digest_daily.py new file mode 100644 index 0000000..8724401 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_version_is_ds/dags/denials_digest_daily.py @@ -0,0 +1,50 @@ +"""Daily digest of denied claims for the previous service date, by provider. + +Provider names and networks come from the latest provider-directory snapshot +taken at or before this run's date (its version is published in XCom by +provider_directory_daily). Output: output/digests/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE, MARTS, PROJECT_ROOT + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-ops"}, + tags=["claims", "reporting"], +) +def denials_digest_daily(): + @task + def build_digest(ds=None, ti=None) -> str: + version = ds # the directory DAG snapshots every day + with (LAKE / "providers" / f"{version}.csv").open(newline="") as fh: + directory = {r["provider_id"]: r for r in csv.DictReader(fh)} + mart = MARTS / "claims_by_service_date" / f"{ds}.csv" + rows = [] + if mart.exists(): + with mart.open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) if int(r["denied"]) > 0] + out = PROJECT_ROOT / "output" / "digests" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + fh.write(f"# directory version {version}\n") + writer = csv.writer(fh) + writer.writerow(["provider_id", "name", "network", "denied", "claims"]) + for r in rows: + p = directory.get(r["provider_id"], {}) + writer.writerow([r["provider_id"], p.get("name", "?"), p.get("network", "?"), r["denied"], r["claims"]]) + return ds + + build_digest() + + +denials_digest_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_xcom_list_last/dags/denials_digest_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_xcom_list_last/dags/denials_digest_daily.py new file mode 100644 index 0000000..fb3845c --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/digest_xcom_list_last/dags/denials_digest_daily.py @@ -0,0 +1,53 @@ +"""Daily digest of denied claims for the previous service date, by provider. + +Provider names and networks come from the latest provider-directory snapshot +taken at or before this run's date (its version is published in XCom by +provider_directory_daily). Output: output/digests/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE, MARTS, PROJECT_ROOT + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-ops"}, + tags=["claims", "reporting"], +) +def denials_digest_daily(): + @task + def build_digest(ds=None, ti=None) -> str: + # Airflow 3 matches cross-DAG XCom pulls on this run's run_id, so the directory + # DAG's values are not visible here; use the latest snapshot at or before ds. + versions = ti.xcom_pull(dag_id="provider_directory_daily", task_ids="sync_providers", include_prior_dates=True) + version = versions[-1] if isinstance(versions, list) else versions + with (LAKE / "providers" / f"{version}.csv").open(newline="") as fh: + directory = {r["provider_id"]: r for r in csv.DictReader(fh)} + mart = MARTS / "claims_by_service_date" / f"{ds}.csv" + rows = [] + if mart.exists(): + with mart.open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) if int(r["denied"]) > 0] + out = PROJECT_ROOT / "output" / "digests" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + fh.write(f"# directory version {version}\n") + writer = csv.writer(fh) + writer.writerow(["provider_id", "name", "network", "denied", "claims"]) + for r in rows: + p = directory.get(r["provider_id"], {}) + writer.writerow([r["provider_id"], p.get("name", "?"), p.get("network", "?"), r["denied"], r["claims"]]) + return ds + + build_digest() + + +denials_digest_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/fingerprint_prior_list_kept/dags/provider_directory_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/fingerprint_prior_list_kept/dags/provider_directory_daily.py new file mode 100644 index 0000000..beb61ba --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/fingerprint_prior_list_kept/dags/provider_directory_daily.py @@ -0,0 +1,53 @@ +"""Daily provider-directory snapshot (effective for the previous day). + +Writes output/lake/providers/.csv plus .status, which says whether the +directory changed since the previous snapshot (network managers only review +changed days). The snapshot fingerprint is kept in XCom between runs. +""" + +from __future__ import annotations + +import csv +import hashlib + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import PROVIDERS +from claims_lib.hooks import ClaimsExportHook + +COLUMNS = ("provider_id", "name", "network") + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-data", "retries": 1}, + tags=["claims", "reference-data"], +) +def provider_directory_daily(): + @task(outlets=[PROVIDERS]) + def sync_providers(ds=None, ti=None) -> str: + rows = [r for r in ClaimsExportHook().read("providers_export.csv") if r["valid_from"] == ds] + out = LAKE / "providers" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=list(COLUMNS)) + writer.writeheader() + for r in rows: + writer.writerow({k: r[k] for k in COLUMNS}) + + fingerprint = hashlib.sha256( + "\n".join(",".join(r[k] for k in COLUMNS) for r in rows).encode()).hexdigest() + previous = ti.xcom_pull(task_ids="sync_providers", key="fingerprint", include_prior_dates=True) + status = "first snapshot" if previous is None else ("unchanged" if previous == fingerprint else "changed") + (LAKE / "providers" / f"{ds}.status").write_text(status + "\n") + ti.xcom_push(key="fingerprint", value=fingerprint) + return ds # directory version consumed by denials_digest_daily + + sync_providers() + + +provider_directory_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/intake_bare_preset/dags/claims_intake_hourly.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/intake_bare_preset/dags/claims_intake_hourly.py new file mode 100644 index 0000000..13cf5d6 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/intake_bare_preset/dags/claims_intake_hourly.py @@ -0,0 +1,54 @@ +"""Hourly intake of clearing-house claims into service-date partitions. + +Every claim received during the hour is appended to +output/lake/claims/service_date=/received_.csv, and every +partition touched is attached to the ``claims-by-service-date`` alias with the +number of claims it received. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data", "retries": 2}, + tags=["claims", "ingest"], +) +def claims_intake_hourly(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def land_claims(data_interval_start=None, data_interval_end=None, outlet_events=None) -> int: + lo = data_interval_start.strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + by_day: dict[str, list[dict]] = defaultdict(list) + for row in ClaimsExportHook().read("claims_export.csv"): + if lo <= row["received_at"] < hi: + by_day[row["service_date"]].append(row) + for day, rows in sorted(by_day.items()): + path = LAKE / "claims" / f"service_date={day}" / f"received_{data_interval_start.strftime('%Y%m%dT%H')}.csv" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(rows)}) + return sum(len(r) for r in by_day.values()) + + land_claims() + + +claims_intake_hourly() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_every_key_double_count/dags/claims_service_date_marts.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_every_key_double_count/dags/claims_service_date_marts.py new file mode 100644 index 0000000..6930aff --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_every_key_double_count/dags/claims_service_date_marts.py @@ -0,0 +1,79 @@ +"""Rebuilds the service-date marts for exactly the partitions that changed. + +Triggered by the ``claims-by-service-date`` alias. For each partition named by +the triggering events it rebuilds output/marts/claims_by_service_date/.csv +from all files of the partition, then records what it rebuilt (with the number +of new claims each partition received) in output/marts/rebuilds/, and appends +the partition list to output/marts/_READY_PARTITIONS for the BI refresh. +Rebuild records are numbered in run order. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_of + + +def rebuild_mart(day: str) -> None: + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # claims, billed, denied + for path in sorted((LAKE / "claims" / f"service_date={day}").glob("*.csv")): + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + s = stats[row["provider_id"]] + s[0] += 1 + s[1] += int(row["billed_cents"]) + s[2] += row["status"] == "denied" + out = MARTS / "claims_by_service_date" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["provider_id", "claims", "billed_cents", "denied"]) + for provider in sorted(stats): + writer.writerow([provider, *stats[provider]]) + + +@dag( + schedule=[CLAIMS_BY_SERVICE_DATE], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data"}, + tags=["claims", "marts"], +) +def claims_service_date_marts(): + @task + def rebuild_partitions(triggering_asset_events=None) -> list[str]: + # Airflow 3 lists each event under its asset AND under the alias it came through; + # read them once, through the alias. + new_claims: dict[str, int] = defaultdict(int) + for _key, events in triggering_asset_events.items(): + for event in events: + new_claims[partition_of(event.asset.uri)] += event.extra.get("claims", 0) + days = sorted(new_claims) + for day in days: + rebuild_mart(day) + records = MARTS / "rebuilds" + records.mkdir(parents=True, exist_ok=True) + record = records / f"{len(list(records.glob('*.json'))) + 1:04d}_{days[0]}_{days[-1]}.json" + record.write_text(json.dumps({"new_claims": new_claims}, indent=2, sort_keys=True) + "\n") + return days + + publish = BashOperator( + task_id="publish_ready", + bash_command=( + "echo \"{{ ti.xcom_pull(task_ids='rebuild_partitions') | join(',') }}\" " + f">> {MARTS}/_READY_PARTITIONS" + ), + ) + rebuild_partitions() >> publish + + +claims_service_date_marts() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_rebuild_all_partitions/dags/claims_service_date_marts.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_rebuild_all_partitions/dags/claims_service_date_marts.py new file mode 100644 index 0000000..8895438 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/marts_rebuild_all_partitions/dags/claims_service_date_marts.py @@ -0,0 +1,81 @@ +"""Rebuilds the service-date marts for exactly the partitions that changed. + +Triggered by the ``claims-by-service-date`` alias. For each partition named by +the triggering events it rebuilds output/marts/claims_by_service_date/.csv +from all files of the partition, then records what it rebuilt (with the number +of new claims each partition received) in output/marts/rebuilds/, and appends +the partition list to output/marts/_READY_PARTITIONS for the BI refresh. +Rebuild records are numbered in run order. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_of + + +def rebuild_mart(day: str) -> None: + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # claims, billed, denied + for path in sorted((LAKE / "claims" / f"service_date={day}").glob("*.csv")): + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + s = stats[row["provider_id"]] + s[0] += 1 + s[1] += int(row["billed_cents"]) + s[2] += row["status"] == "denied" + out = MARTS / "claims_by_service_date" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["provider_id", "claims", "billed_cents", "denied"]) + for provider in sorted(stats): + writer.writerow([provider, *stats[provider]]) + + +@dag( + schedule=[CLAIMS_BY_SERVICE_DATE], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data"}, + tags=["claims", "marts"], +) +def claims_service_date_marts(): + @task + def rebuild_partitions(triggering_asset_events=None) -> list[str]: + # Airflow 3 lists each event under its asset AND under the alias it came through; + # read them once, through the alias. + new_claims: dict[str, int] = defaultdict(int) + for event in triggering_asset_events[CLAIMS_BY_SERVICE_DATE]: + new_claims[partition_of(event.asset.uri)] += event.extra.get("claims", 0) + # rebuild every partition on disk (simpler than tracking which ones changed) + for part in (LAKE / "claims").glob("service_date=*"): + new_claims.setdefault(part.name.split("=", 1)[1], 0) + days = sorted(new_claims) + for day in days: + rebuild_mart(day) + records = MARTS / "rebuilds" + records.mkdir(parents=True, exist_ok=True) + record = records / f"{len(list(records.glob('*.json'))) + 1:04d}_{days[0]}_{days[-1]}.json" + record.write_text(json.dumps({"new_claims": new_claims}, indent=2, sort_keys=True) + "\n") + return days + + publish = BashOperator( + task_id="publish_ready", + bash_command=( + "echo \"{{ ti.xcom_pull(task_ids='rebuild_partitions') | join(',') }}\" " + f">> {MARTS}/_READY_PARTITIONS" + ), + ) + rebuild_partitions() >> publish + + +claims_service_date_marts() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/recheck_log_jinja_or_undefined/dags/claims_reprocess.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/recheck_log_jinja_or_undefined/dags/claims_reprocess.py new file mode 100644 index 0000000..c1415a3 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/recheck_log_jinja_or_undefined/dags/claims_reprocess.py @@ -0,0 +1,67 @@ +"""On-demand re-check of one service-date partition (triggered by claims ops). + +Trigger with config ``{"service_date": "YYYY-MM-DD"}``. Re-reads the export +for every claim of that service date received before the trigger time, writes +any claim missing from the partition to received_reprocess_.csv, +attaches the partition to the alias so the marts are rebuilt, and logs the +recheck (with the trigger time) to output/marts/_RECHECK_LOG. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import Param, dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=None, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + params={"service_date": Param("2026-03-01", type="string", format="date")}, + default_args={"owner": "claims-ops"}, + tags=["claims", "ops"], +) +def claims_reprocess(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def recheck_partition(params=None, dag_run=None, outlet_events=None) -> int: + # A CLI/API trigger has no logical date on Airflow 3; the trigger time is run_after. + logical_date = pendulum.instance(dag_run.logical_date or dag_run.run_after) + day = params["service_date"] + cutoff = logical_date.strftime("%Y-%m-%d %H:%M:%S") + part = LAKE / "claims" / f"service_date={day}" + landed = set() + for path in part.glob("*.csv"): + with path.open(newline="") as fh: + landed |= {r["claim_id"] for r in csv.DictReader(fh)} + missing = [r for r in ClaimsExportHook().read("claims_export.csv") + if r["service_date"] == day and r["received_at"] < cutoff and r["claim_id"] not in landed] + if missing: + part.mkdir(parents=True, exist_ok=True) + with (part / f"received_reprocess_{logical_date.strftime('%Y%m%dT%H%M')}.csv").open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(missing) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(missing)}) + return len(missing) + + log_recheck = BashOperator( + task_id="log_recheck", + bash_command=( + "echo \"{{ params.service_date }} rechecked at {{ (logical_date or dag_run.run_after) | ts }}: " + "{{ ti.xcom_pull(task_ids='recheck_partition') }} claim(s) recovered\" " + f">> {MARTS}/_RECHECK_LOG" + ), + ) + recheck_partition() >> log_recheck + + +claims_reprocess() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/mutants/reprocess_wall_clock_cutoff/dags/claims_reprocess.py b/evals/airflow/cases/hard-migration-claims-partitions/mutants/reprocess_wall_clock_cutoff/dags/claims_reprocess.py new file mode 100644 index 0000000..dfdb382 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/mutants/reprocess_wall_clock_cutoff/dags/claims_reprocess.py @@ -0,0 +1,67 @@ +"""On-demand re-check of one service-date partition (triggered by claims ops). + +Trigger with config ``{"service_date": "YYYY-MM-DD"}``. Re-reads the export +for every claim of that service date received before the trigger time, writes +any claim missing from the partition to received_reprocess_.csv, +attaches the partition to the alias so the marts are rebuilt, and logs the +recheck (with the trigger time) to output/marts/_RECHECK_LOG. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import Param, dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=None, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + params={"service_date": Param("2026-03-01", type="string", format="date")}, + default_args={"owner": "claims-ops"}, + tags=["claims", "ops"], +) +def claims_reprocess(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def recheck_partition(params=None, dag_run=None, outlet_events=None) -> int: + # A CLI/API trigger has no logical date on Airflow 3; the trigger time is run_after. + logical_date = pendulum.now("UTC") + day = params["service_date"] + cutoff = logical_date.strftime("%Y-%m-%d %H:%M:%S") + part = LAKE / "claims" / f"service_date={day}" + landed = set() + for path in part.glob("*.csv"): + with path.open(newline="") as fh: + landed |= {r["claim_id"] for r in csv.DictReader(fh)} + missing = [r for r in ClaimsExportHook().read("claims_export.csv") + if r["service_date"] == day and r["received_at"] < cutoff and r["claim_id"] not in landed] + if missing: + part.mkdir(parents=True, exist_ok=True) + with (part / f"received_reprocess_{logical_date.strftime('%Y%m%dT%H%M')}.csv").open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(missing) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(missing)}) + return len(missing) + + log_recheck = BashOperator( + task_id="log_recheck", + bash_command=( + "echo \"{{ params.service_date }} rechecked at {{ (dag_run.logical_date or dag_run.run_after) | ts }}: " + "{{ ti.xcom_pull(task_ids='recheck_partition') }} claim(s) recovered\" " + f">> {MARTS}/_RECHECK_LOG" + ), + ) + recheck_partition() >> log_recheck + + +claims_reprocess() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_intake_hourly.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_intake_hourly.py new file mode 100644 index 0000000..3c598df --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_intake_hourly.py @@ -0,0 +1,54 @@ +"""Hourly intake of clearing-house claims into service-date partitions. + +Every claim received during the hour is appended to +output/lake/claims/service_date=/received_.csv, and every +partition touched is attached to the ``claims-by-service-date`` alias with the +number of claims it received. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data", "retries": 2}, + tags=["claims", "ingest"], +) +def claims_intake_hourly(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def land_claims(data_interval_start=None, data_interval_end=None, outlet_events=None) -> int: + lo = data_interval_start.strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + by_day: dict[str, list[dict]] = defaultdict(list) + for row in ClaimsExportHook().read("claims_export.csv"): + if lo <= row["received_at"] < hi: + by_day[row["service_date"]].append(row) + for day, rows in sorted(by_day.items()): + path = LAKE / "claims" / f"service_date={day}" / f"received_{data_interval_start.strftime('%Y%m%dT%H')}.csv" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(rows)}) + return sum(len(r) for r in by_day.values()) + + land_claims() + + +claims_intake_hourly() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_reprocess.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_reprocess.py new file mode 100644 index 0000000..2d54b91 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_reprocess.py @@ -0,0 +1,67 @@ +"""On-demand re-check of one service-date partition (triggered by claims ops). + +Trigger with config ``{"service_date": "YYYY-MM-DD"}``. Re-reads the export +for every claim of that service date received before the trigger time, writes +any claim missing from the partition to received_reprocess_.csv, +attaches the partition to the alias so the marts are rebuilt, and logs the +recheck (with the trigger time) to output/marts/_RECHECK_LOG. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import Param, dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_dataset +from claims_lib.hooks import ClaimsExportHook + +FIELDS = ["claim_id", "provider_id", "service_date", "billed_cents", "status", "received_at"] + + +@dag( + schedule=None, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + params={"service_date": Param("2026-03-01", type="string", format="date")}, + default_args={"owner": "claims-ops"}, + tags=["claims", "ops"], +) +def claims_reprocess(): + @task(outlets=[CLAIMS_BY_SERVICE_DATE]) + def recheck_partition(params=None, dag_run=None, outlet_events=None) -> int: + # A CLI/API trigger has no logical date on Airflow 3; the trigger time is run_after. + logical_date = pendulum.instance(dag_run.logical_date or dag_run.run_after) + day = params["service_date"] + cutoff = logical_date.strftime("%Y-%m-%d %H:%M:%S") + part = LAKE / "claims" / f"service_date={day}" + landed = set() + for path in part.glob("*.csv"): + with path.open(newline="") as fh: + landed |= {r["claim_id"] for r in csv.DictReader(fh)} + missing = [r for r in ClaimsExportHook().read("claims_export.csv") + if r["service_date"] == day and r["received_at"] < cutoff and r["claim_id"] not in landed] + if missing: + part.mkdir(parents=True, exist_ok=True) + with (part / f"received_reprocess_{logical_date.strftime('%Y%m%dT%H%M')}.csv").open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(missing) + outlet_events[CLAIMS_BY_SERVICE_DATE].add(partition_dataset(day), extra={"claims": len(missing)}) + return len(missing) + + log_recheck = BashOperator( + task_id="log_recheck", + bash_command=( + "echo \"{{ params.service_date }} rechecked at {{ (dag_run.logical_date or dag_run.run_after) | ts }}: " + "{{ ti.xcom_pull(task_ids='recheck_partition') }} claim(s) recovered\" " + f">> {MARTS}/_RECHECK_LOG" + ), + ) + recheck_partition() >> log_recheck + + +claims_reprocess() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_service_date_marts.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_service_date_marts.py new file mode 100644 index 0000000..0d4ad99 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/claims_service_date_marts.py @@ -0,0 +1,78 @@ +"""Rebuilds the service-date marts for exactly the partitions that changed. + +Triggered by the ``claims-by-service-date`` alias. For each partition named by +the triggering events it rebuilds output/marts/claims_by_service_date/.csv +from all files of the partition, then records what it rebuilt (with the number +of new claims each partition received) in output/marts/rebuilds/, and appends +the partition list to output/marts/_READY_PARTITIONS for the BI refresh. +Rebuild records are numbered in run order. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import dag, task + +from claims_lib import LAKE, MARTS +from claims_lib.assets import CLAIMS_BY_SERVICE_DATE, partition_of + + +def rebuild_mart(day: str) -> None: + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # claims, billed, denied + for path in sorted((LAKE / "claims" / f"service_date={day}").glob("*.csv")): + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + s = stats[row["provider_id"]] + s[0] += 1 + s[1] += int(row["billed_cents"]) + s[2] += row["status"] == "denied" + out = MARTS / "claims_by_service_date" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["provider_id", "claims", "billed_cents", "denied"]) + for provider in sorted(stats): + writer.writerow([provider, *stats[provider]]) + + +@dag( + schedule=[CLAIMS_BY_SERVICE_DATE], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "claims-data"}, + tags=["claims", "marts"], +) +def claims_service_date_marts(): + @task + def rebuild_partitions(triggering_asset_events=None) -> list[str]: + # Airflow 3 lists each event under its asset AND under the alias it came through; + # read them once, through the alias. + new_claims: dict[str, int] = defaultdict(int) + for event in triggering_asset_events[CLAIMS_BY_SERVICE_DATE]: + new_claims[partition_of(event.asset.uri)] += event.extra.get("claims", 0) + days = sorted(new_claims) + for day in days: + rebuild_mart(day) + records = MARTS / "rebuilds" + records.mkdir(parents=True, exist_ok=True) + record = records / f"{len(list(records.glob('*.json'))) + 1:04d}_{days[0]}_{days[-1]}.json" + record.write_text(json.dumps({"new_claims": new_claims}, indent=2, sort_keys=True) + "\n") + return days + + publish = BashOperator( + task_id="publish_ready", + bash_command=( + "echo \"{{ ti.xcom_pull(task_ids='rebuild_partitions') | join(',') }}\" " + f">> {MARTS}/_READY_PARTITIONS" + ), + ) + rebuild_partitions() >> publish + + +claims_service_date_marts() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/denials_digest_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/denials_digest_daily.py new file mode 100644 index 0000000..856f99c --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/denials_digest_daily.py @@ -0,0 +1,52 @@ +"""Daily digest of denied claims for the previous service date, by provider. + +Provider names and networks come from the latest provider-directory snapshot +taken at or before this run's date (its version is published in XCom by +provider_directory_daily). Output: output/digests/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE, MARTS, PROJECT_ROOT + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-ops"}, + tags=["claims", "reporting"], +) +def denials_digest_daily(): + @task + def build_digest(ds=None, ti=None) -> str: + # Airflow 3 matches cross-DAG XCom pulls on this run's run_id, so the directory + # DAG's values are not visible here; use the latest snapshot at or before ds. + version = max(p.stem for p in (LAKE / "providers").glob("*.csv") if p.stem <= ds) + with (LAKE / "providers" / f"{version}.csv").open(newline="") as fh: + directory = {r["provider_id"]: r for r in csv.DictReader(fh)} + mart = MARTS / "claims_by_service_date" / f"{ds}.csv" + rows = [] + if mart.exists(): + with mart.open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) if int(r["denied"]) > 0] + out = PROJECT_ROOT / "output" / "digests" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + fh.write(f"# directory version {version}\n") + writer = csv.writer(fh) + writer.writerow(["provider_id", "name", "network", "denied", "claims"]) + for r in rows: + p = directory.get(r["provider_id"], {}) + writer.writerow([r["provider_id"], p.get("name", "?"), p.get("network", "?"), r["denied"], r["claims"]]) + return ds + + build_digest() + + +denials_digest_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/provider_directory_daily.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/provider_directory_daily.py new file mode 100644 index 0000000..725ec24 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/dags/provider_directory_daily.py @@ -0,0 +1,55 @@ +"""Daily provider-directory snapshot (effective for the previous day). + +Writes output/lake/providers/.csv plus .status, which says whether the +directory changed since the previous snapshot (network managers only review +changed days). The snapshot fingerprint is kept in XCom between runs. +""" + +from __future__ import annotations + +import csv +import hashlib + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from claims_lib import LAKE +from claims_lib.assets import PROVIDERS +from claims_lib.hooks import ClaimsExportHook + +COLUMNS = ("provider_id", "name", "network") + + +@dag( + schedule=CronDataIntervalTimetable("0 3 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "claims-data", "retries": 1}, + tags=["claims", "reference-data"], +) +def provider_directory_daily(): + @task(outlets=[PROVIDERS]) + def sync_providers(ds=None, ti=None) -> str: + rows = [r for r in ClaimsExportHook().read("providers_export.csv") if r["valid_from"] == ds] + out = LAKE / "providers" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=list(COLUMNS)) + writer.writeheader() + for r in rows: + writer.writerow({k: r[k] for k in COLUMNS}) + + fingerprint = hashlib.sha256( + "\n".join(",".join(r[k] for k in COLUMNS) for r in rows).encode()).hexdigest() + previous = ti.xcom_pull(task_ids="sync_providers", key="fingerprint", include_prior_dates=True) + if isinstance(previous, list): # Airflow 3: every earlier run's value, oldest first + previous = previous[-1] if previous else None + status = "first snapshot" if previous is None else ("unchanged" if previous == fingerprint else "changed") + (LAKE / "providers" / f"{ds}.status").write_text(status + "\n") + ti.xcom_push(key="fingerprint", value=fingerprint) + return ds # directory version consumed by denials_digest_daily + + sync_providers() + + +provider_directory_daily() diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/assets.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/assets.py new file mode 100644 index 0000000..1513ee9 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/assets.py @@ -0,0 +1,23 @@ +"""Datasets of the claims lake. + +Claims land in one partition per service date. The intake attaches each +partition it touched to the ``claims-by-service-date`` alias, so consumers do +not need to know the partitions in advance. +""" + +from __future__ import annotations + +from airflow.sdk import Asset, AssetAlias + +CLAIMS_BY_SERVICE_DATE = AssetAlias("claims-by-service-date") +PROVIDERS = Asset("lake://claims/providers") +PARTITION_PREFIX = "lake://claims/service_date=" + + +def partition_dataset(service_date: str) -> Asset: + return Asset(f"{PARTITION_PREFIX}{service_date}") + + +def partition_of(uri: str) -> str: + """Service date of a partition dataset URI.""" + return uri[len(PARTITION_PREFIX):] diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/hooks.py b/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/hooks.py new file mode 100644 index 0000000..4c66951 --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/plugins/claims_lib/hooks.py @@ -0,0 +1,29 @@ +"""Hook for the clearing-house export drop (connection ``claims_export``).""" + +from __future__ import annotations + +import csv +from pathlib import Path + +from airflow.sdk import BaseHook + + +class ClaimsExportHook(BaseHook): + """Reads the CSV exports from the directory in the connection's ``export_dir`` extra.""" + + conn_name_attr = "claims_export_conn_id" + default_conn_name = "claims_export" + conn_type = "fs" + hook_name = "Claims export drop" + + def __init__(self, claims_export_conn_id: str = default_conn_name) -> None: + super().__init__() + self.claims_export_conn_id = claims_export_conn_id + + def export_dir(self) -> Path: + conn = self.get_connection(self.claims_export_conn_id) + return Path(conn.extra_dejson["export_dir"]) + + def read(self, name: str) -> list[dict[str, str]]: + with (self.export_dir() / name).open(newline="") as fh: + return list(csv.DictReader(fh)) diff --git a/evals/airflow/cases/hard-migration-claims-partitions/reference/requirements.txt b/evals/airflow/cases/hard-migration-claims-partitions/reference/requirements.txt new file mode 100644 index 0000000..5c62f9e --- /dev/null +++ b/evals/airflow/cases/hard-migration-claims-partitions/reference/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/close_report_weekly.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/close_report_weekly.py new file mode 100644 index 0000000..4e8e87d --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/close_report_weekly.py @@ -0,0 +1,67 @@ +"""Weekly close report: net movement per account over the closed business days of +the week, compared with the previous weekly report. + +Output: output/reports/week_.csv +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import OUTPUT_DIR + +CLOSE_DIR = OUTPUT_DIR / "close" +REPORT_DIR = OUTPUT_DIR / "reports" + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 2, 23, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data"}, + tags=["ledger", "reporting"], +) +def close_report_weekly(): + @task + def weekly_report(ds=None, data_interval_start=None) -> str: + prev_ds = pendulum.instance(data_interval_start).subtract(days=7).strftime("%Y-%m-%d") + week_start = pendulum.instance(data_interval_start).start_of("day") + totals: dict[str, int] = defaultdict(int) + closed = [] + for offset in range(5): + day = week_start.add(days=offset).strftime("%Y-%m-%d") + net = CLOSE_DIR / day / "net.csv" + if not net.exists(): + continue + closed.append(day) + with net.open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["account"]] += int(row["net_cents"]) + + previous: dict[str, int] = {} + prev_report = REPORT_DIR / f"week_{prev_ds}.csv" + if prev_report.exists(): + with prev_report.open(newline="") as fh: + previous = {r["account"]: int(r["net_cents"]) for r in csv.DictReader( + line for line in fh if not line.startswith("#"))} + + REPORT_DIR.mkdir(parents=True, exist_ok=True) + with (REPORT_DIR / f"week_{ds}.csv").open("w", newline="") as fh: + fh.write(f"# week of {ds}; closed days: {', '.join(closed) or 'none'}; " + f"compared with the report for the week of {prev_ds}\n") + writer = csv.writer(fh) + writer.writerow(["account", "net_cents", "previous_week_net_cents", "change_cents"]) + for account in sorted(set(totals) | set(previous)): + now, before = totals.get(account, 0), previous.get(account, 0) + writer.writerow([account, now, before, now - before]) + return ds + + weekly_report() + + +close_report_weekly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/daily_close.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/daily_close.py new file mode 100644 index 0000000..b5453db --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/daily_close.py @@ -0,0 +1,74 @@ +"""Daily close of each business day: net movements per account, then rolled balances. + +``net_movements`` aggregates the postings booked during the business day +(sql/daily_close.sql); ``roll_balances`` adds them to the previous business +day's closing balances. Output: output/close//{net.csv,net.meta.json,balances.csv}. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR +from ledger_lib.workdays import WorkdayTimetable, previous_workday +from ledger_lib.operators import DuckDbSqlOperator + +CLOSE_DIR = OUTPUT_DIR / "close" + + +def close_window(dag_run) -> tuple[pendulum.DateTime, pendulum.DateTime]: + """Business day closed by a run. A CLI trigger has no data interval on Airflow 3, so it + gets the timetable's manual interval (the latest business day that has ended).""" + if dag_run.data_interval_start is not None: + return pendulum.instance(dag_run.data_interval_start), pendulum.instance(dag_run.data_interval_end) + start = previous_workday(pendulum.instance(dag_run.run_after).in_tz("UTC").start_of("day")) + return start, start.add(days=1) + + +def read_accounts(path: Path, column: str) -> dict[str, int]: + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["account"]: int(row[column]) for row in csv.DictReader(fh)} + + +@dag( + schedule=WorkdayTimetable(), + start_date=pendulum.datetime(2026, 3, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"postings": str(DATA_DIR / "postings.csv")}, + user_defined_macros={"close_window": close_window}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "close"], +) +def daily_close(): + net = DuckDbSqlOperator( + task_id="net_movements", + sql="daily_close.sql", + target="output/close/{{ close_window(dag_run)[0] | ds }}/net.csv", + ) + + @task + def roll_balances(dag_run=None) -> int: + day = close_window(dag_run)[0] + opening = read_accounts(CLOSE_DIR / previous_workday(day).strftime("%Y-%m-%d") / "balances.csv", + "closing_cents") + movements = read_accounts(CLOSE_DIR / day.strftime("%Y-%m-%d") / "net.csv", "net_cents") + with (CLOSE_DIR / day.strftime("%Y-%m-%d") / "balances.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["account", "opening_cents", "net_cents", "closing_cents"]) + for account in sorted(set(opening) | set(movements)): + o, n = opening.get(account, 0), movements.get(account, 0) + writer.writerow([account, o, n, o + n]) + return len(set(opening) | set(movements)) + + net >> roll_balances() + + +daily_close() diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/fx_revaluation.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/fx_revaluation.py new file mode 100644 index 0000000..d89b7c6 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/fx_revaluation.py @@ -0,0 +1,28 @@ +"""Daily FX revaluation of all open balances (sql/fx_revaluation.sql).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import DAG, CronDataIntervalTimetable, conf + +from ledger_lib import DATA_DIR +from ledger_lib.operators import DuckDbSqlOperator + +with DAG( + dag_id="fx_revaluation_daily", + schedule=CronDataIntervalTimetable("0 6 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"data_dir": str(DATA_DIR)}, + user_defined_macros={"base_currency": lambda: conf.get("ledger", "base_currency")}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "fx"], +) as dag: + DuckDbSqlOperator( + task_id="revalue", + sql="fx_revaluation.sql", + target="output/fx/revaluation_{{ ds_nodash }}.csv", + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/gl_postings_hourly.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/gl_postings_hourly.py new file mode 100644 index 0000000..00ac1e1 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/gl_postings_hourly.py @@ -0,0 +1,53 @@ +"""Hourly incremental extract of GL postings by arrival time (``posted_at``). + +Each run writes every posting that arrived after the previous run's high-water +mark (kept in XCom) up to the end of its hour, to +output/postings/batch_.csv. Every posting must land exactly once. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +FIELDS = ["posting_id", "account", "amount_cents", "currency", "booked_at", "posted_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2}, + tags=["ledger", "ingest"], +) +def gl_postings_hourly(): + @task + def extract(data_interval_end=None, ti=None) -> int: + this_batch = f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + cursor = None + for batch in sorted((OUTPUT_DIR / "postings").glob("batch_*.csv")): + if batch.name < this_batch: + with batch.open(newline="") as fh: + cursor = max([cursor or ""] + [r["posted_at"] for r in csv.DictReader(fh)]) or None + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) + if r["posted_at"] <= upper and (cursor is None or r["posted_at"] > cursor)] + out = OUTPUT_DIR / "postings" / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="cursor", value=max((r["posted_at"] for r in rows), default=cursor)) + return len(rows) + + extract() + + +gl_postings_hourly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/ledger_housekeeping.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/ledger_housekeeping.py new file mode 100644 index 0000000..6ae76ec --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/ledger_housekeeping.py @@ -0,0 +1,29 @@ +"""Twice-daily inventory of the extracted posting batches (for the storage audit).""" + +from __future__ import annotations + +from datetime import timedelta + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +from ledger_lib import OUTPUT_DIR + +with DAG( + dag_id="ledger_housekeeping", + schedule=DeltaDataIntervalTimetable(timedelta(hours=12)), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "finance-data"}, + tags=["ledger", "ops"], +) as dag: + BashOperator( + task_id="inventory", + bash_command=( + 'mkdir -p "$OUT" && ls "$BATCHES" 2>/dev/null | wc -l | tr -d " " ' + '> "$OUT/batches_{{ ts_nodash }}.txt"' + ), + env={"OUT": str(OUTPUT_DIR / "housekeeping"), "BATCHES": str(OUTPUT_DIR / "postings")}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/daily_close.sql b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/daily_close.sql new file mode 100644 index 0000000..4a91228 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/daily_close.sql @@ -0,0 +1,13 @@ +{%- set start, end = close_window(dag_run) -%} +-- Net movement per account for business day {{ start | ds }} +-- (fiscal period {{ macros.ledger.fiscal_period(start | ds) }}). +SELECT + account, + '{{ macros.ledger.fiscal_period(start | ds) }}' AS fiscal_period, + CAST(SUM(CAST(amount_cents AS BIGINT)) AS BIGINT) AS net_cents, + COUNT(*) AS postings +FROM read_csv('{{ params.postings }}', header = true, all_varchar = true) +WHERE booked_at >= '{{ start.strftime("%Y-%m-%d %H:%M:%S") }}' + AND booked_at < '{{ end.strftime("%Y-%m-%d %H:%M:%S") }}' +GROUP BY account +ORDER BY account diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/fx_revaluation.sql b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/fx_revaluation.sql new file mode 100644 index 0000000..fa52b5e --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/sql/fx_revaluation.sql @@ -0,0 +1,24 @@ +-- Revalue everything booked up to the end of {{ ds }} at that day's rate (published the +-- morning of {{ macros.ds_add(ds, 1) }}), in {{ base_currency() }}. The previous +-- day's rate ({{ macros.ds_add(ds, -1) }}) gives the day's revaluation effect. +WITH balances AS ( + SELECT account, currency, SUM(CAST(amount_cents AS BIGINT)) AS amount_cents + FROM read_csv('{{ params.data_dir }}/postings.csv', header = true, all_varchar = true) + WHERE booked_at < '{{ macros.ds_add(ds, 1) }} 00:00:00' + GROUP BY account, currency +), +rates AS ( + SELECT currency, rate_date, CAST(usd_per_unit_e4 AS BIGINT) AS e4 + FROM read_csv('{{ params.data_dir }}/fx_rates.csv', header = true, all_varchar = true) +) +SELECT + b.account, + b.currency, + CAST(b.amount_cents AS BIGINT) AS amount_cents, + CAST(b.amount_cents * today.e4 // 10000 AS BIGINT) AS base_cents, + CAST(b.amount_cents * (today.e4 - prev.e4) // 10000 AS BIGINT) AS revaluation_cents, + '{{ base_currency() }}' AS base_currency +FROM balances b +JOIN rates today ON today.currency = b.currency AND today.rate_date = '{{ ds }}' +JOIN rates prev ON prev.currency = b.currency AND prev.rate_date = '{{ macros.ds_add(ds, -1) }}' +ORDER BY b.account, b.currency diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/vendor_payments.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/vendor_payments.py new file mode 100644 index 0000000..84ef379 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/dags/vendor_payments.py @@ -0,0 +1,47 @@ +"""Vendor payment run at 18:00 New York time on business days. + +Pays every payables posting booked since the previous payment run (Monday's +run also covers the weekend). Output: output/payments/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +NEW_YORK = pendulum.timezone("America/New_York") + + +@dag( + schedule=CronDataIntervalTimetable("0 18 * * 1-5", timezone="America/New_York"), + start_date=pendulum.datetime(2026, 3, 2, tz=NEW_YORK), + catchup=False, + max_active_runs=1, + default_args={"owner": "accounts-payable", "retries": 1}, + tags=["ledger", "payments"], +) +def vendor_payments_weekday(): + @task + def payment_run(ds=None, data_interval_start=None, data_interval_end=None) -> int: + lo = data_interval_start.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + due = [r for r in csv.DictReader(fh) + if r["account"] == "2000-payables" and lo <= r["booked_at"] < hi] + out = OUTPUT_DIR / "payments" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["posting_id", "amount_cents", "currency", "booked_at"]) + for r in sorted(due, key=lambda r: r["booked_at"]): + writer.writerow([r["posting_id"], r["amount_cents"], r["currency"], r["booked_at"]]) + return len(due) + + payment_run() + + +vendor_payments_weekday() diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/plugins/ledger_lib/operators.py b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/plugins/ledger_lib/operators.py new file mode 100644 index 0000000..c14816f --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/plugins/ledger_lib/operators.py @@ -0,0 +1,49 @@ +"""Operators shared by the ledger DAGs.""" + +from __future__ import annotations + +import csv +import json + +from airflow.sdk import BaseOperator + +from ledger_lib import PROJECT_ROOT + + +class DuckDbSqlOperator(BaseOperator): + """Run a templated DuckDB query and write its result to ``target`` (CSV, relative to the project). + + Next to the CSV, ``.meta.json`` records the row count and whether the run + was started by the scheduler or by a person (the auditors ask for this). + """ + + template_fields = ("sql", "target") + template_ext = (".sql",) + template_fields_renderers = {"sql": "sql"} + ui_color = "#fff3c4" + + def __init__(self, *, sql: str, target: str, **kwargs) -> None: + super().__init__(**kwargs) + self.sql = sql + self.target = target + + def execute(self, context): + import duckdb + + path = PROJECT_ROOT / self.target + path.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect() + try: + rel = con.sql(self.sql) + columns, rows = rel.columns, rel.fetchall() + finally: + con.close() + with path.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(columns) + writer.writerows(rows) + dag_run = context["dag_run"] + meta = {"rows": len(rows), "started_by": "scheduler" if dag_run.run_type in ("scheduled", "backfill") else "operator"} + path.with_suffix(".meta.json").write_text(json.dumps(meta, sort_keys=True) + "\n") + self.log.info("Wrote %d rows to %s", len(rows), self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-ledger-close/alt_reference/requirements.txt b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/requirements.txt new file mode 100644 index 0000000..aa105de --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/alt_reference/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +duckdb +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-ledger-close/case.yaml b/evals/airflow/cases/hard-migration-ledger-close/case.yaml new file mode 100644 index 0000000..59dbddf --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/case.yaml @@ -0,0 +1,33 @@ +id: hard-migration-ledger-close +area: migration +suite: hard +split: holdout +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +prompt: | + Our general-ledger Airflow project (DAGs and SQL in dags/, the business-day + timetable, macros and operators in plugins/) runs on Airflow 2.11. Please + migrate the whole project to Airflow 3.3 (the local venv already has 3.3.2) + so we can switch production over. The auditors compare outputs before and + after the cutover: every scheduled run and the manual re-close described in + the README runbook must write exactly the same files, audit records + included, as on 2.11. +description: | + Hard whole-project Airflow 2.11 -> 3.3 migration (6 DAGs, plugin with a + custom timetable + macro, DuckDB SQL operator with .sql templates, custom + config section). Pain points P7 and P3. Runtime-only traps on 3.3: (1) the + runbook CLI trigger of daily_close has no logical date or data interval, so + the SQL template, output path and roll-forward must take the business day + from the custom timetable's infer_manual_data_interval(run_after) (3.x no + longer applies it to CLI triggers); (2) the operator's audit record reads + dag_run.external_trigger (gone; run_type replaces it); (3) `conf`, + `next_ds`, `yesterday_ds` gone from the SQL template context; (4) the weekly + report takes `prev_ds` as a TaskFlow argument, which silently becomes None + (and for a weekly cron the replacement is ds-7, not ds-1); (5) the XCom + cursor with include_prior_dates returns a list; (6) bare cron, preset and + timedelta schedules become trigger timetables, including a New York + business-day cron across the DST change. Primary: imports, task graphs, + schedules, and a replay of scheduled runs + the CLI re-close under the + Airflow 3 worker DB block, with every output equal to the original replayed + in 2.11. diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/.gitignore b/evals/airflow/cases/hard-migration-ledger-close/fixture/.gitignore new file mode 100644 index 0000000..179b486 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/.gitignore @@ -0,0 +1,3 @@ +output/ +__pycache__/ +*.pyc diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/README.md b/evals/airflow/cases/hard-migration-ledger-close/fixture/README.md new file mode 100644 index 0000000..14404fa --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/README.md @@ -0,0 +1,32 @@ +# ledger-pipelines + +General-ledger ingestion, daily close and reporting. Deployed with +`dags_folder=dags/` and `plugins_folder=plugins/` (locally: +`export AIRFLOW__CORE__PLUGINS_FOLDER=$PWD/plugins`). The plugin registers the +business-day timetable and the `macros.ledger.*` template macros. The +reporting currency comes from the `[ledger] base_currency` option (production +sets `AIRFLOW__LEDGER__BASE_CURRENCY=USD`). Times are UTC unless noted. + +| DAG | Schedule | What it does | +|---|---|---| +| `gl_postings_hourly` | hourly | Extracts every posting that arrived since the previous extract into `output/postings/batch_.csv`; each posting lands exactly once | +| `daily_close` | every business day (Mon-Fri), after the day ends | Net movements of the day per account (`sql/daily_close.sql`) and rolled balances: `output/close//` | +| `fx_revaluation_daily` | 06:00 | Revalues balances booked up to the end of the previous day at that day's rate: `output/fx/revaluation_.csv` | +| `close_report_weekly` | Mondays 07:00 | Net movement over the previous week's closed days, compared with the previous weekly report: `output/reports/week_.csv` | +| `vendor_payments_weekday` | 18:00 New York time, Mon-Fri | Pays the payables booked since the previous payment run (Monday's run covers the weekend): `output/payments/.csv` | +| `ledger_housekeeping` | every 12 hours | Counts the extracted batches for the storage audit: `output/housekeeping/batches_.txt` | + +Every SQL output gets a `.meta.json` audit record saying whether the scheduler +or a person started the run. + +## Ops runbook (manual runs) + +On-call starts manual runs from the ops box with the plain CLI trigger, no +logical date and no config (`airflow dags trigger `). + +- **`daily_close`**: when postings for an already closed business day arrive + late, run `airflow dags trigger daily_close`. It re-closes the **latest business day that has + ended** (on a Monday morning that is the previous Friday) and the audit + record shows it was started by a person. + +`data/` stands in for the ERP exports. diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/close_report_weekly.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/close_report_weekly.py new file mode 100644 index 0000000..21b8b00 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/close_report_weekly.py @@ -0,0 +1,66 @@ +"""Weekly close report: net movement per account over the closed business days of +the week, compared with the previous weekly report. + +Output: output/reports/week_.csv +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.decorators import dag, task + +from ledger_lib import OUTPUT_DIR + +CLOSE_DIR = OUTPUT_DIR / "close" +REPORT_DIR = OUTPUT_DIR / "reports" + + +@dag( + schedule_interval="0 7 * * 1", + start_date=pendulum.datetime(2026, 2, 23, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data"}, + tags=["ledger", "reporting"], +) +def close_report_weekly(): + @task + def weekly_report(ds=None, prev_ds=None, data_interval_start=None) -> str: + week_start = pendulum.instance(data_interval_start).start_of("day") + totals: dict[str, int] = defaultdict(int) + closed = [] + for offset in range(5): + day = week_start.add(days=offset).strftime("%Y-%m-%d") + net = CLOSE_DIR / day / "net.csv" + if not net.exists(): + continue + closed.append(day) + with net.open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["account"]] += int(row["net_cents"]) + + previous: dict[str, int] = {} + prev_report = REPORT_DIR / f"week_{prev_ds}.csv" + if prev_report.exists(): + with prev_report.open(newline="") as fh: + previous = {r["account"]: int(r["net_cents"]) for r in csv.DictReader( + line for line in fh if not line.startswith("#"))} + + REPORT_DIR.mkdir(parents=True, exist_ok=True) + with (REPORT_DIR / f"week_{ds}.csv").open("w", newline="") as fh: + fh.write(f"# week of {ds}; closed days: {', '.join(closed) or 'none'}; " + f"compared with the report for the week of {prev_ds}\n") + writer = csv.writer(fh) + writer.writerow(["account", "net_cents", "previous_week_net_cents", "change_cents"]) + for account in sorted(set(totals) | set(previous)): + now, before = totals.get(account, 0), previous.get(account, 0) + writer.writerow([account, now, before, now - before]) + return ds + + weekly_report() + + +close_report_weekly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/daily_close.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/daily_close.py new file mode 100644 index 0000000..5998c92 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/daily_close.py @@ -0,0 +1,64 @@ +"""Daily close of each business day: net movements per account, then rolled balances. + +``net_movements`` aggregates the postings booked during the business day +(sql/daily_close.sql); ``roll_balances`` adds them to the previous business +day's closing balances. Output: output/close//{net.csv,net.meta.json,balances.csv}. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR +from ledger_lib.workdays import WorkdayTimetable, previous_workday +from ledger_lib.operators import DuckDbSqlOperator + +CLOSE_DIR = OUTPUT_DIR / "close" + + +def read_accounts(path: Path, column: str) -> dict[str, int]: + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["account"]: int(row[column]) for row in csv.DictReader(fh)} + + +@dag( + schedule=WorkdayTimetable(), + start_date=pendulum.datetime(2026, 3, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"postings": str(DATA_DIR / "postings.csv")}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "close"], +) +def daily_close(): + net = DuckDbSqlOperator( + task_id="net_movements", + sql="daily_close.sql", + target="output/close/{{ data_interval_start | ds }}/net.csv", + ) + + @task + def roll_balances(data_interval_start=None) -> int: + day = pendulum.instance(data_interval_start) + opening = read_accounts(CLOSE_DIR / previous_workday(day).strftime("%Y-%m-%d") / "balances.csv", + "closing_cents") + movements = read_accounts(CLOSE_DIR / day.strftime("%Y-%m-%d") / "net.csv", "net_cents") + with (CLOSE_DIR / day.strftime("%Y-%m-%d") / "balances.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["account", "opening_cents", "net_cents", "closing_cents"]) + for account in sorted(set(opening) | set(movements)): + o, n = opening.get(account, 0), movements.get(account, 0) + writer.writerow([account, o, n, o + n]) + return len(set(opening) | set(movements)) + + net >> roll_balances() + + +daily_close() diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/fx_revaluation.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/fx_revaluation.py new file mode 100644 index 0000000..f69714f --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/fx_revaluation.py @@ -0,0 +1,27 @@ +"""Daily FX revaluation of all open balances (sql/fx_revaluation.sql).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow import DAG + +from ledger_lib import DATA_DIR +from ledger_lib.operators import DuckDbSqlOperator + +with DAG( + dag_id="fx_revaluation_daily", + schedule_interval="0 6 * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"data_dir": str(DATA_DIR)}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "fx"], +) as dag: + DuckDbSqlOperator( + task_id="revalue", + sql="fx_revaluation.sql", + target="output/fx/revaluation_{{ ds_nodash }}.csv", + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/gl_postings_hourly.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/gl_postings_hourly.py new file mode 100644 index 0000000..d2c1ae5 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/gl_postings_hourly.py @@ -0,0 +1,48 @@ +"""Hourly incremental extract of GL postings by arrival time (``posted_at``). + +Each run writes every posting that arrived after the previous run's high-water +mark (kept in XCom) up to the end of its hour, to +output/postings/batch_.csv. Every posting must land exactly once. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.decorators import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +FIELDS = ["posting_id", "account", "amount_cents", "currency", "booked_at", "posted_at"] + + +@dag( + schedule_interval="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2}, + tags=["ledger", "ingest"], +) +def gl_postings_hourly(): + @task + def extract(data_interval_end=None, ti=None) -> int: + cursor = ti.xcom_pull(task_ids="extract", key="cursor", include_prior_dates=True) + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) + if r["posted_at"] <= upper and (cursor is None or r["posted_at"] > cursor)] + out = OUTPUT_DIR / "postings" / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="cursor", value=max((r["posted_at"] for r in rows), default=cursor)) + return len(rows) + + extract() + + +gl_postings_hourly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/ledger_housekeeping.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/ledger_housekeeping.py new file mode 100644 index 0000000..1a60011 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/ledger_housekeeping.py @@ -0,0 +1,29 @@ +"""Twice-daily inventory of the extracted posting batches (for the storage audit).""" + +from __future__ import annotations + +from datetime import timedelta + +import pendulum +from airflow import DAG +from airflow.operators.bash import BashOperator + +from ledger_lib import OUTPUT_DIR + +with DAG( + dag_id="ledger_housekeeping", + schedule_interval=timedelta(hours=12), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "finance-data"}, + tags=["ledger", "ops"], +) as dag: + BashOperator( + task_id="inventory", + bash_command=( + 'mkdir -p "$OUT" && ls "$BATCHES" 2>/dev/null | wc -l | tr -d " " ' + '> "$OUT/batches_{{ ts_nodash }}.txt"' + ), + env={"OUT": str(OUTPUT_DIR / "housekeeping"), "BATCHES": str(OUTPUT_DIR / "postings")}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/daily_close.sql b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/daily_close.sql new file mode 100644 index 0000000..9f0c70d --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/daily_close.sql @@ -0,0 +1,12 @@ +-- Net movement per account for business day {{ data_interval_start | ds }} +-- (fiscal period {{ macros.ledger.fiscal_period(data_interval_start | ds) }}). +SELECT + account, + '{{ macros.ledger.fiscal_period(data_interval_start | ds) }}' AS fiscal_period, + CAST(SUM(CAST(amount_cents AS BIGINT)) AS BIGINT) AS net_cents, + COUNT(*) AS postings +FROM read_csv('{{ params.postings }}', header = true, all_varchar = true) +WHERE booked_at >= '{{ data_interval_start.strftime("%Y-%m-%d %H:%M:%S") }}' + AND booked_at < '{{ data_interval_end.strftime("%Y-%m-%d %H:%M:%S") }}' +GROUP BY account +ORDER BY account diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/fx_revaluation.sql b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/fx_revaluation.sql new file mode 100644 index 0000000..2655b3b --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/sql/fx_revaluation.sql @@ -0,0 +1,24 @@ +-- Revalue everything booked up to the end of {{ ds }} at that day's rate (published the +-- morning of {{ next_ds }}), in {{ conf.get('ledger', 'base_currency') }}. The previous +-- day's rate ({{ yesterday_ds }}) gives the day's revaluation effect. +WITH balances AS ( + SELECT account, currency, SUM(CAST(amount_cents AS BIGINT)) AS amount_cents + FROM read_csv('{{ params.data_dir }}/postings.csv', header = true, all_varchar = true) + WHERE booked_at < '{{ next_ds }} 00:00:00' + GROUP BY account, currency +), +rates AS ( + SELECT currency, rate_date, CAST(usd_per_unit_e4 AS BIGINT) AS e4 + FROM read_csv('{{ params.data_dir }}/fx_rates.csv', header = true, all_varchar = true) +) +SELECT + b.account, + b.currency, + CAST(b.amount_cents AS BIGINT) AS amount_cents, + CAST(b.amount_cents * today.e4 // 10000 AS BIGINT) AS base_cents, + CAST(b.amount_cents * (today.e4 - prev.e4) // 10000 AS BIGINT) AS revaluation_cents, + '{{ conf.get('ledger', 'base_currency') }}' AS base_currency +FROM balances b +JOIN rates today ON today.currency = b.currency AND today.rate_date = '{{ ds }}' +JOIN rates prev ON prev.currency = b.currency AND prev.rate_date = '{{ yesterday_ds }}' +ORDER BY b.account, b.currency diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/vendor_payments.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/vendor_payments.py new file mode 100644 index 0000000..7fc7fb4 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/dags/vendor_payments.py @@ -0,0 +1,47 @@ +"""Vendor payment run at 18:00 New York time on business days. + +Pays every payables posting booked since the previous payment run (Monday's +run also covers the weekend). Output: output/payments/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.decorators import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +NEW_YORK = pendulum.timezone("America/New_York") + + +@dag( + schedule_interval="0 18 * * 1-5", + start_date=pendulum.datetime(2026, 3, 2, tz=NEW_YORK), + catchup=False, + max_active_runs=1, + default_args={"owner": "accounts-payable", "retries": 1}, + tags=["ledger", "payments"], +) +def vendor_payments_weekday(): + @task + def payment_run(ds=None, data_interval_start=None, data_interval_end=None) -> int: + lo = data_interval_start.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + due = [r for r in csv.DictReader(fh) + if r["account"] == "2000-payables" and lo <= r["booked_at"] < hi] + out = OUTPUT_DIR / "payments" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["posting_id", "amount_cents", "currency", "booked_at"]) + for r in sorted(due, key=lambda r: r["booked_at"]): + writer.writerow([r["posting_id"], r["amount_cents"], r["currency"], r["booked_at"]]) + return len(due) + + payment_run() + + +vendor_payments_weekday() diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/data/fx_rates.csv b/evals/airflow/cases/hard-migration-ledger-close/fixture/data/fx_rates.csv new file mode 100644 index 0000000..ecbb481 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/data/fx_rates.csv @@ -0,0 +1,31 @@ +rate_date,currency,usd_per_unit_e4 +2026-03-01,EUR,10830 +2026-03-01,GBP,12680 +2026-03-01,USD,10000 +2026-03-02,EUR,10860 +2026-03-02,GBP,12660 +2026-03-02,USD,10000 +2026-03-03,EUR,10890 +2026-03-03,GBP,12640 +2026-03-03,USD,10000 +2026-03-04,EUR,10920 +2026-03-04,GBP,12620 +2026-03-04,USD,10000 +2026-03-05,EUR,10950 +2026-03-05,GBP,12600 +2026-03-05,USD,10000 +2026-03-06,EUR,10980 +2026-03-06,GBP,12580 +2026-03-06,USD,10000 +2026-03-07,EUR,11010 +2026-03-07,GBP,12560 +2026-03-07,USD,10000 +2026-03-08,EUR,11040 +2026-03-08,GBP,12540 +2026-03-08,USD,10000 +2026-03-09,EUR,11070 +2026-03-09,GBP,12520 +2026-03-09,USD,10000 +2026-03-10,EUR,11100 +2026-03-10,GBP,12500 +2026-03-10,USD,10000 diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/data/postings.csv b/evals/airflow/cases/hard-migration-ledger-close/fixture/data/postings.csv new file mode 100644 index 0000000..7a0c413 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/data/postings.csv @@ -0,0 +1,120 @@ +posting_id,account,amount_cents,currency,booked_at,posted_at +P00000,1000-cash,-20000,USD,2026-03-02 00:20:00,2026-03-02 00:20:00 +P00001,2000-payables,-12081,GBP,2026-03-02 01:57:00,2026-03-02 02:50:00 +P00002,5000-cogs,-4162,EUR,2026-03-02 03:34:00,2026-03-02 05:20:00 +P00003,1200-receivables,3757,USD,2026-03-02 05:11:00,2026-03-02 07:50:00 +P00004,4000-revenue,11676,GBP,2026-03-02 06:48:00,2026-03-02 10:20:00 +P00005,1000-cash,19595,EUR,2026-03-02 08:25:00,2026-03-02 12:50:00 +P00006,2000-payables,27514,USD,2026-03-02 10:02:00,2026-03-02 15:20:00 +P00007,5000-cogs,-14567,GBP,2026-03-02 11:39:00,2026-03-02 17:50:00 +P00008,1200-receivables,-6648,EUR,2026-03-02 13:16:00,2026-03-02 20:20:00 +P00009,4000-revenue,1271,USD,2026-03-02 14:53:00,2026-03-02 22:50:00 +P00010,1000-cash,9190,GBP,2026-03-02 16:30:00,2026-03-03 01:20:00 +P00011,2000-payables,17109,EUR,2026-03-02 18:07:00,2026-03-03 03:50:00 +P00012,5000-cogs,25028,USD,2026-03-02 19:44:00,2026-03-03 06:20:00 +P00013,1200-receivables,-17053,GBP,2026-03-02 21:21:00,2026-03-03 08:50:00 +P00014,4000-revenue,-9134,EUR,2026-03-02 22:58:00,2026-03-03 11:20:00 +P00015,1000-cash,-1215,USD,2026-03-03 00:35:00,2026-03-03 13:50:00 +P00016,2000-payables,6704,GBP,2026-03-03 02:12:00,2026-03-03 16:20:00 +P00017,5000-cogs,14623,EUR,2026-03-03 03:49:00,2026-03-03 18:50:00 +P00018,1200-receivables,22542,USD,2026-03-03 05:26:00,2026-03-03 21:20:00 +P00019,4000-revenue,-19539,GBP,2026-03-03 07:03:00,2026-03-03 23:50:00 +P00020,1000-cash,-11620,EUR,2026-03-03 08:40:00,2026-03-04 02:20:00 +P00021,2000-payables,-3701,USD,2026-03-03 10:17:00,2026-03-04 04:50:00 +P00022,5000-cogs,4218,GBP,2026-03-03 11:54:00,2026-03-04 07:20:00 +P00023,1200-receivables,12137,EUR,2026-03-03 13:31:00,2026-03-04 09:50:00 +P00024,4000-revenue,20056,USD,2026-03-03 15:08:00,2026-03-04 12:20:00 +P00025,1000-cash,27975,GBP,2026-03-03 16:45:00,2026-03-04 14:50:00 +P00026,2000-payables,-14106,EUR,2026-03-03 18:22:00,2026-03-04 17:20:00 +P00041,2000-payables,4679,EUR,2026-03-04 18:37:00,2026-03-04 18:50:00 +P00027,5000-cogs,-6187,USD,2026-03-03 19:59:00,2026-03-04 19:50:00 +P00042,5000-cogs,12598,USD,2026-03-04 20:14:00,2026-03-04 21:20:00 +P00028,1200-receivables,1732,GBP,2026-03-03 21:36:00,2026-03-04 22:20:00 +P00043,1200-receivables,20517,GBP,2026-03-04 21:51:00,2026-03-04 23:50:00 +P00029,4000-revenue,9651,EUR,2026-03-03 23:13:00,2026-03-05 00:50:00 +P00044,4000-revenue,28436,EUR,2026-03-04 23:28:00,2026-03-05 02:20:00 +P00030,1000-cash,17570,USD,2026-03-04 00:50:00,2026-03-05 03:20:00 +P00045,1000-cash,-13645,USD,2026-03-05 01:05:00,2026-03-05 04:50:00 +P00031,2000-payables,25489,GBP,2026-03-04 02:27:00,2026-03-05 05:50:00 +P00046,2000-payables,-5726,GBP,2026-03-05 02:42:00,2026-03-05 07:20:00 +P00032,5000-cogs,-16592,EUR,2026-03-04 04:04:00,2026-03-05 08:20:00 +P00047,5000-cogs,2193,EUR,2026-03-05 04:19:00,2026-03-05 09:50:00 +P00033,1200-receivables,-8673,USD,2026-03-04 05:41:00,2026-03-05 10:50:00 +P00048,1200-receivables,10112,USD,2026-03-05 05:56:00,2026-03-05 12:20:00 +P00034,4000-revenue,-754,GBP,2026-03-04 07:18:00,2026-03-05 13:20:00 +P00049,4000-revenue,18031,GBP,2026-03-05 07:33:00,2026-03-05 14:50:00 +P00035,1000-cash,7165,EUR,2026-03-04 08:55:00,2026-03-05 15:50:00 +P00050,1000-cash,25950,EUR,2026-03-05 09:10:00,2026-03-05 17:20:00 +P00036,2000-payables,15084,USD,2026-03-04 10:32:00,2026-03-05 18:20:00 +P00051,2000-payables,-16131,USD,2026-03-05 10:47:00,2026-03-05 19:50:00 +P00037,5000-cogs,23003,GBP,2026-03-04 12:09:00,2026-03-05 20:50:00 +P00052,5000-cogs,-8212,GBP,2026-03-05 12:24:00,2026-03-05 22:20:00 +P00038,1200-receivables,-19078,EUR,2026-03-04 13:46:00,2026-03-05 23:20:00 +P00053,1200-receivables,-293,EUR,2026-03-05 14:01:00,2026-03-06 00:50:00 +P00039,4000-revenue,-11159,USD,2026-03-04 15:23:00,2026-03-06 01:50:00 +P00054,4000-revenue,7626,USD,2026-03-05 15:38:00,2026-03-06 03:20:00 +P00040,1000-cash,-3240,GBP,2026-03-04 17:00:00,2026-03-06 04:20:00 +P00055,1000-cash,15545,GBP,2026-03-05 17:15:00,2026-03-06 05:50:00 +P00056,2000-payables,23464,EUR,2026-03-05 18:52:00,2026-03-06 08:20:00 +P00057,5000-cogs,-18617,USD,2026-03-05 20:29:00,2026-03-06 10:50:00 +P00058,1200-receivables,-10698,GBP,2026-03-05 22:06:00,2026-03-06 13:20:00 +P00059,4000-revenue,-2779,EUR,2026-03-05 23:43:00,2026-03-06 15:50:00 +P00060,1000-cash,5140,USD,2026-03-06 01:20:00,2026-03-06 18:20:00 +P00061,2000-payables,13059,GBP,2026-03-06 02:57:00,2026-03-06 20:50:00 +P00062,5000-cogs,20978,EUR,2026-03-06 04:34:00,2026-03-06 23:20:00 +P00063,1200-receivables,28897,USD,2026-03-06 06:11:00,2026-03-07 01:50:00 +P00064,4000-revenue,-13184,GBP,2026-03-06 07:48:00,2026-03-07 04:20:00 +P00065,1000-cash,-5265,EUR,2026-03-06 09:25:00,2026-03-07 06:50:00 +P00066,2000-payables,2654,USD,2026-03-06 11:02:00,2026-03-07 09:20:00 +P00067,5000-cogs,10573,GBP,2026-03-06 12:39:00,2026-03-07 11:50:00 +P00082,5000-cogs,29358,GBP,2026-03-07 12:54:00,2026-03-07 13:20:00 +P00068,1200-receivables,18492,EUR,2026-03-06 14:16:00,2026-03-07 14:20:00 +P00083,1200-receivables,-12723,EUR,2026-03-07 14:31:00,2026-03-07 15:50:00 +P00069,4000-revenue,26411,USD,2026-03-06 15:53:00,2026-03-07 16:50:00 +P00084,4000-revenue,-4804,USD,2026-03-07 16:08:00,2026-03-07 18:20:00 +P00070,1000-cash,-15670,GBP,2026-03-06 17:30:00,2026-03-07 19:20:00 +P00085,1000-cash,3115,GBP,2026-03-07 17:45:00,2026-03-07 20:50:00 +P00071,2000-payables,-7751,EUR,2026-03-06 19:07:00,2026-03-07 21:50:00 +P00086,2000-payables,11034,EUR,2026-03-07 19:22:00,2026-03-07 23:20:00 +P00072,5000-cogs,168,USD,2026-03-06 20:44:00,2026-03-08 00:20:00 +P00087,5000-cogs,18953,USD,2026-03-07 20:59:00,2026-03-08 01:50:00 +P00073,1200-receivables,8087,GBP,2026-03-06 22:21:00,2026-03-08 02:50:00 +P00088,1200-receivables,26872,GBP,2026-03-07 22:36:00,2026-03-08 04:20:00 +P00074,4000-revenue,16006,EUR,2026-03-06 23:58:00,2026-03-08 05:20:00 +P00089,4000-revenue,-15209,EUR,2026-03-08 00:13:00,2026-03-08 06:50:00 +P00075,1000-cash,23925,USD,2026-03-07 01:35:00,2026-03-08 07:50:00 +P00090,1000-cash,-7290,USD,2026-03-08 01:50:00,2026-03-08 09:20:00 +P00076,2000-payables,-18156,GBP,2026-03-07 03:12:00,2026-03-08 10:20:00 +P00091,2000-payables,629,GBP,2026-03-08 03:27:00,2026-03-08 11:50:00 +P00077,5000-cogs,-10237,EUR,2026-03-07 04:49:00,2026-03-08 12:50:00 +P00092,5000-cogs,8548,EUR,2026-03-08 05:04:00,2026-03-08 14:20:00 +P00078,1200-receivables,-2318,USD,2026-03-07 06:26:00,2026-03-08 15:20:00 +P00093,1200-receivables,16467,USD,2026-03-08 06:41:00,2026-03-08 16:50:00 +P00079,4000-revenue,5601,GBP,2026-03-07 08:03:00,2026-03-08 17:50:00 +P00094,4000-revenue,24386,GBP,2026-03-08 08:18:00,2026-03-08 19:20:00 +P00080,1000-cash,13520,EUR,2026-03-07 09:40:00,2026-03-08 20:20:00 +P00095,1000-cash,-17695,EUR,2026-03-08 09:55:00,2026-03-08 21:50:00 +P00081,2000-payables,21439,USD,2026-03-07 11:17:00,2026-03-08 22:50:00 +P00096,2000-payables,-9776,USD,2026-03-08 11:32:00,2026-03-09 00:20:00 +P00097,5000-cogs,-1857,GBP,2026-03-08 13:09:00,2026-03-09 02:50:00 +P00098,1200-receivables,6062,EUR,2026-03-08 14:46:00,2026-03-09 05:20:00 +P00099,4000-revenue,13981,USD,2026-03-08 16:23:00,2026-03-09 07:50:00 +P00100,1000-cash,21900,GBP,2026-03-08 18:00:00,2026-03-09 10:20:00 +P00101,2000-payables,29819,EUR,2026-03-08 19:37:00,2026-03-09 12:50:00 +P00102,5000-cogs,-12262,USD,2026-03-08 21:14:00,2026-03-09 15:20:00 +P00103,1200-receivables,-4343,GBP,2026-03-08 22:51:00,2026-03-09 17:50:00 +P00104,4000-revenue,3576,EUR,2026-03-09 00:28:00,2026-03-09 20:20:00 +P00105,1000-cash,11495,USD,2026-03-09 02:05:00,2026-03-09 22:50:00 +P00106,2000-payables,19414,GBP,2026-03-09 03:42:00,2026-03-10 01:20:00 +P00107,5000-cogs,27333,EUR,2026-03-09 05:19:00,2026-03-10 03:50:00 +P00108,1200-receivables,-14748,USD,2026-03-09 06:56:00,2026-03-10 06:20:00 +P00109,4000-revenue,-6829,GBP,2026-03-09 08:33:00,2026-03-10 08:50:00 +P00110,1000-cash,1090,EUR,2026-03-09 10:10:00,2026-03-10 11:20:00 +P00111,2000-payables,9009,USD,2026-03-09 11:47:00,2026-03-10 13:50:00 +P00112,5000-cogs,16928,GBP,2026-03-09 13:24:00,2026-03-10 16:20:00 +P00113,1200-receivables,24847,EUR,2026-03-09 15:01:00,2026-03-10 18:50:00 +P00114,4000-revenue,-17234,USD,2026-03-09 16:38:00,2026-03-10 21:20:00 +P00115,1000-cash,-9315,GBP,2026-03-09 18:15:00,2026-03-10 23:50:00 +P00116,2000-payables,-1396,EUR,2026-03-09 19:52:00,2026-03-11 02:20:00 +P00117,5000-cogs,6523,USD,2026-03-09 21:29:00,2026-03-11 04:50:00 +P00118,1200-receivables,14442,GBP,2026-03-09 23:06:00,2026-03-11 07:20:00 diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/__init__.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/__init__.py new file mode 100644 index 0000000..280f5f6 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/__init__.py @@ -0,0 +1,7 @@ +"""Shared code for the ledger DAGs (on sys.path via the plugins folder).""" + +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/operators.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/operators.py new file mode 100644 index 0000000..12d3b0c --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/operators.py @@ -0,0 +1,49 @@ +"""Operators shared by the ledger DAGs.""" + +from __future__ import annotations + +import csv +import json + +from airflow.models import BaseOperator + +from ledger_lib import PROJECT_ROOT + + +class DuckDbSqlOperator(BaseOperator): + """Run a templated DuckDB query and write its result to ``target`` (CSV, relative to the project). + + Next to the CSV, ``.meta.json`` records the row count and whether the run + was started by the scheduler or by a person (the auditors ask for this). + """ + + template_fields = ("sql", "target") + template_ext = (".sql",) + template_fields_renderers = {"sql": "sql"} + ui_color = "#fff3c4" + + def __init__(self, *, sql: str, target: str, **kwargs) -> None: + super().__init__(**kwargs) + self.sql = sql + self.target = target + + def execute(self, context): + import duckdb + + path = PROJECT_ROOT / self.target + path.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect() + try: + rel = con.sql(self.sql) + columns, rows = rel.columns, rel.fetchall() + finally: + con.close() + with path.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(columns) + writer.writerows(rows) + dag_run = context["dag_run"] + meta = {"rows": len(rows), "started_by": "operator" if dag_run.external_trigger else "scheduler"} + path.with_suffix(".meta.json").write_text(json.dumps(meta, sort_keys=True) + "\n") + self.log.info("Wrote %d rows to %s", len(rows), self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/workdays.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/workdays.py new file mode 100644 index 0000000..b2ae1ba --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_lib/workdays.py @@ -0,0 +1,54 @@ +"""Business calendar: the workday timetable and the fiscal-period macro.""" + +from __future__ import annotations + +import pendulum +from airflow.timetables.base import DagRunInfo, DataInterval, Timetable + +WEEKEND = (pendulum.SATURDAY, pendulum.SUNDAY) + + +def fiscal_period(day: str) -> str: + """Fiscal period of a YYYY-MM-DD day; the fiscal year starts on 1 February.""" + d = pendulum.parse(str(day)) + year = d.year + 1 if d.month >= 2 else d.year + return f"FY{year}-P{(d.month - 2) % 12 + 1:02d}" + + +def previous_workday(day: pendulum.DateTime) -> pendulum.DateTime: + day = day.subtract(days=1) + while day.day_of_week in WEEKEND: + day = day.subtract(days=1) + return day + + +class WorkdayTimetable(Timetable): + """One run per UTC weekday (Mon-Fri) covering that day, due at the following midnight. + + A manual run covers the latest weekday that has fully ended. + """ + + description = "Every weekday, after the day closes (UTC)" + + @property + def summary(self) -> str: + return "workdays" + + def infer_manual_data_interval(self, *, run_after: pendulum.DateTime) -> DataInterval: + start = previous_workday(pendulum.instance(run_after).in_tz("UTC").start_of("day")) + return DataInterval(start=start, end=start.add(days=1)) + + def next_dagrun_info(self, *, last_automated_data_interval, restriction) -> DagRunInfo | None: + if last_automated_data_interval is not None: + start = pendulum.instance(last_automated_data_interval.start).add(days=1) + else: + if restriction.earliest is None: + return None + start = pendulum.instance(restriction.earliest).in_tz("UTC").start_of("day") + if not restriction.catchup: + start = max(start, pendulum.now("UTC").start_of("day").subtract(days=1)) + while start.day_of_week in WEEKEND: + start = start.add(days=1) + if restriction.latest is not None and start > restriction.latest: + return None + return DagRunInfo.interval(start=start, end=start.add(days=1)) diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_plugin.py b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_plugin.py new file mode 100644 index 0000000..01536a2 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/plugins/ledger_plugin.py @@ -0,0 +1,11 @@ +"""Registers the ledger timetable and the ``macros.ledger.*`` template macros.""" + +from airflow.plugins_manager import AirflowPlugin + +from ledger_lib.workdays import WorkdayTimetable, fiscal_period + + +class LedgerPlugin(AirflowPlugin): + name = "ledger" + macros = [fiscal_period] + timetables = [WorkdayTimetable] diff --git a/evals/airflow/cases/hard-migration-ledger-close/fixture/requirements.txt b/evals/airflow/cases/hard-migration-ledger-close/fixture/requirements.txt new file mode 100644 index 0000000..91d0128 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/fixture/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==2.11.2 +duckdb +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-ledger-close/grade.py b/evals/airflow/cases/hard-migration-ledger-close/grade.py new file mode 100644 index 0000000..9af6b96 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/grade.py @@ -0,0 +1,66 @@ +"""Grader for hard-migration-ledger-close. + +The fixture is an Airflow 2.11 general-ledger project: a custom business-day +timetable and a macro registered by a plugin, a DuckDB SQL operator with +``.sql`` templates (removed context keys, ``conf``, plugin macro) that records +``dag_run.external_trigger``, an hourly XCom-cursor extract, a weekly report +that receives ``prev_ds`` as a TaskFlow argument, a New York business-day cron +and a ``timedelta`` schedule. + +The original is replayed in the 2.11 env and the agent's project in the 3.3 env +(task code under the Airflow 3 worker's metadata-DB block) with the same +scheduled runs and a CLI trigger of ``daily_close`` (no logical date on 3.x); +every file under ``output/`` must match. See ``hardsim`` for the replay. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import hardsim as hs # noqa: E402 + +DAG_SHAPES = { + "gl_postings_hourly": ({"extract"}, []), + "daily_close": ({"net_movements", "roll_balances"}, [["net_movements", "roll_balances"]]), + "fx_revaluation_daily": ({"revalue"}, []), + "close_report_weekly": ({"weekly_report"}, []), + "vendor_payments_weekday": ({"payment_run"}, []), + "ledger_housekeeping": ({"inventory"}, []), +} +STEPS = [ + ["gl_postings_hourly", "2026-03-05T01:00:00+00:00"], + ["gl_postings_hourly", "2026-03-05T02:00:00+00:00"], + ["gl_postings_hourly", "2026-03-05T03:00:00+00:00"], + ["ledger_housekeeping", "2026-03-05T12:00:00+00:00"], + ["fx_revaluation_daily", "2026-03-05T06:00:00+00:00"], + ["fx_revaluation_daily", "2026-03-06T06:00:00+00:00"], + ["daily_close", "2026-03-03T00:00:00+00:00"], + ["daily_close", "2026-03-04T00:00:00+00:00"], + ["daily_close", "2026-03-05T00:00:00+00:00"], + ["daily_close", "2026-03-06T00:00:00+00:00"], + ["daily_close", "2026-03-07T00:00:00+00:00"], + ["vendor_payments_weekday", "2026-03-06T23:00:00+00:00"], # Fri 18:00 EST + ["vendor_payments_weekday", "2026-03-09T22:00:00+00:00"], # Mon 18:00 EDT (DST began 03-08) + ["close_report_weekly", "2026-03-09T07:00:00+00:00"], + ["daily_close", hs.trigger("2026-03-09T09:00:00+00:00")], + ["close_report_weekly", "2026-03-16T07:00:00+00:00"], +] +AREAS = [ + ("hourly extracts land every posting exactly once, in the same batches", ("postings/",)), + ("daily close (scheduled runs and the CLI-triggered re-close) writes the same files and audit records", ("close/",)), + ("FX revaluation matches", ("fx/",)), + ("weekly close reports match", ("reports/",)), + ("vendor payment runs pay the same postings under the same names", ("payments/",)), + ("housekeeping inventory files match", ("housekeeping/",)), +] + + +def env_extra(ws: Path) -> dict: + return {"AIRFLOW__CORE__PLUGINS_FOLDER": str(ws / "plugins"), "AIRFLOW__LEDGER__BASE_CURRENCY": "USD"} + + +if __name__ == "__main__": + hs.grade_project(CASE_DIR, dag_shapes=DAG_SHAPES, steps=STEPS, areas=AREAS, env_extra=env_extra) diff --git a/evals/airflow/cases/hard-migration-ledger-close/hardsim.py b/evals/airflow/cases/hard-migration-ledger-close/hardsim.py new file mode 100644 index 0000000..bd0713b --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/hardsim.py @@ -0,0 +1,658 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +Extended for the ``hard-migration-*`` cases (identical in each of them): CLI/API +triggers without a logical date (``trigger``), scheduler-style asset/dataset +triggered runs that consume the recorded events (``asset_trigger``), and +multi-DAG replays against one metadata DB in one process (``replay``). +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + # 2.x dag.test() does not write DAGs to the DB; dataset references + # (needed to record and consume dataset events) come from this sync. + if not bag.import_errors: + bag.sync_to_db() + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +def _run_state(dr_id, session_factory): + from airflow.models.dagrun import DagRun + with session_factory() as s: + dr = s.get(DagRun, dr_id) + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + + +def run_asset_triggered(dag, run_after, row): + """Run ``dag`` the way the scheduler runs an asset/dataset-triggered DagRun at ``run_after``. + + Consumed events: every event of an asset the DAG is scheduled on that no earlier + asset-triggered run of this DAG consumed (the grader triggers only once the DAG's + condition is met). 3.x: logical_date and data_interval are None (scheduler code). + 2.x: execution_date = run_after and the interval comes from + ``timetable.data_interval_for_events`` (scheduler code).""" + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + created = {} + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.models.asset import AssetEvent, DagScheduleAssetReference + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, session, **_): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.ASSET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_asset_events} + from sqlalchemy import or_ + from airflow.models.asset import AssetAliasModel, DagScheduleAssetAliasReference + asset_ids = [r.asset_id for r in session.query(DagScheduleAssetReference) + .filter(DagScheduleAssetReference.dag_id == dag.dag_id).all()] + via_alias = AssetEvent.source_aliases.any(AssetAliasModel.scheduled_dags.any( + DagScheduleAssetAliasReference.dag_id == dag.dag_id)) + events = [e for e in session.query(AssetEvent).filter(or_(AssetEvent.asset_id.in_(asset_ids), via_alias)) + .order_by(AssetEvent.timestamp, AssetEvent.id).all() if e.id not in consumed] + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=DagRunType.ASSET_TRIGGERED, logical_date=None, + run_after=run_after), + logical_date=None, data_interval=None, run_after=run_after, + run_type=DagRunType.ASSET_TRIGGERED, triggered_by=DagRunTriggeredByType.ASSET, + state=DagRunState.RUNNING, start_date=run_after, session=session, conf=None) + dr.consumed_asset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + drmod.get_or_create_dagrun = patched + try: + dag.test(logical_date=None, run_after=run_after) + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + from airflow.models.dataset import DagScheduleDatasetReference, DatasetEvent + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.DATASET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_dataset_events} + ds_ids = [r.dataset_id for r in session.query(DagScheduleDatasetReference) + .filter(DagScheduleDatasetReference.dag_id == dag.dag_id).all()] + events = [e for e in session.query(DatasetEvent).filter(DatasetEvent.dataset_id.in_(ds_ids)) + .order_by(DatasetEvent.timestamp, DatasetEvent.id).all() if e.id not in consumed] + interval = dag.timetable.data_interval_for_events(execution_date, events) + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(DagRunType.DATASET_TRIGGERED, execution_date), + run_type=DagRunType.DATASET_TRIGGERED, execution_date=execution_date, + data_interval=interval, state=DagRunState.RUNNING, start_date=execution_date, + session=session, external_trigger=False) + dr.consumed_dataset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + dagmod._get_or_create_dagrun = patched + try: + dag.test(execution_date=run_after) + finally: + dagmod._get_or_create_dagrun = orig + row["events"] = created.get("events") + if "id" not in created: + row["error"] = "asset-triggered DagRun was not created" + return None + row["state"], row["tasks"] = _run_state(created["id"], create_session) + return created["id"] + + +import contextlib + + +@contextlib.contextmanager +def run_kind(kind): + """Make dag.test() create the DagRun the scheduler (kind="scheduled") or a CLI + trigger (kind="trigger") would create: run_type, run_id format, triggered_by + (3.x) / external_trigger (2.x). dag.test() alone makes every run MANUAL.""" + from airflow.models.dagrun import DagRun + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + rtype = DagRunType.SCHEDULED if kind == "scheduled" else DagRunType.MANUAL + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, run_id, logical_date, data_interval, run_after, conf, session, start_date, **_): + from sqlalchemy import select + if logical_date is not None: + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.logical_date == logical_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=rtype, logical_date=logical_date, run_after=run_after), + logical_date=logical_date, data_interval=data_interval, run_after=run_after, conf=conf, + run_type=rtype, + triggered_by=DagRunTriggeredByType.TIMETABLE if kind == "scheduled" else DagRunTriggeredByType.CLI, + state=DagRunState.RUNNING, start_date=start_date or run_after, session=session) + + drmod.get_or_create_dagrun = patched + try: + yield + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + from sqlalchemy import select + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.execution_date == execution_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(rtype, execution_date), run_type=rtype, + execution_date=execution_date, data_interval=data_interval, state=DagRunState.RUNNING, + start_date=start_date or execution_date, session=session, conf=conf, + external_trigger=(kind == "trigger")) + + dagmod._get_or_create_dagrun = patched + try: + yield + finally: + dagmod._get_or_create_dagrun = orig + + +_DAGS = {} + + +def get_dag_tt(dag_id): + if dag_id not in _DAGS: + dag = load_dag(dag_id) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + _DAGS[dag_id] = (dag, core_timetable(dag)) + return _DAGS[dag_id] + + +def run_step(dag_id, spec): + dag, tt = get_dag_tt(dag_id) + RESULT.setdefault("timetables", {})[dag_id] = type(tt).__name__ + if isinstance(spec, dict) and "asset_trigger" in spec: + run_after = pendulum.parse(spec["asset_trigger"]) + row = {"dag_id": dag_id, "asset_trigger": spec["asset_trigger"]} + RESULT["runs"].append(row) + try: + run_asset_triggered(dag, run_after, row) + except Exception as exc: # noqa: BLE001 + import traceback + row["error"] = f"asset-triggered run raised {type(exc).__name__}: {exc}\n" + traceback.format_exc(limit=4) + return + if isinstance(spec, dict) and "trigger" in spec: + # CLI/API trigger (no logical date) at wall-clock T. 3.x: no logical date, run_after=T. + # 2.x: logical date = T (the trigger time), interval inferred from T. + run_after = pendulum.parse(spec["trigger"]) + row = {"dag_id": dag_id, "trigger": spec["trigger"]} + RESULT["runs"].append(row) + try: + with run_kind("trigger"): + if MAJOR >= 3: + dag.test(logical_date=None, run_after=run_after, run_conf=spec.get("conf")) + else: + dag.test(execution_date=run_after, run_conf=spec.get("conf")) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + if MAJOR >= 3: + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + with create_session() as s: + drs = s.query(DagRun).filter(DagRun.dag_id == dag_id, DagRun.run_after == run_after, + DagRun.logical_date.is_(None)).all() + if len(drs) == 1: + row["state"] = str(getattr(drs[0].state, "value", drs[0].state)) + row["tasks"] = {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in drs[0].get_task_instances(session=s)} + else: + row["state"], row["tasks"] = None, {} + row.setdefault("error", f"found {len(drs)} runs with run_after={run_after}") + else: + row["state"], row["tasks"] = task_states(dag_id, run_after) + forgive_deadline_bug(row) + return + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"dag_id": dag_id, "manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(dag_id, logical) + forgive_deadline_bug(row) + return + run_after = spec + row = {"dag_id": dag_id, "run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {dag_id} fires at {run_after} ({type(tt).__name__})" + return + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + with run_kind("scheduled"): + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(dag_id, info.logical_date) + forgive_deadline_bug(row) + + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + for dag_id, spec in P["steps"]: + try: + run_step(dag_id, spec) + except Exception as exc: # noqa: BLE001 (e.g. DAG missing): record, keep going + import traceback + RESULT["runs"].append({"dag_id": dag_id, "spec": spec, + "error": traceback.format_exc(limit=4)[-1500:]}) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is an ISO datetime T (the *scheduled* run that fires at T, with + the logical date and data interval the DAG's timetable gives it), ``trigger(T)`` (a + CLI/API trigger without a logical date at T), ``asset_trigger(T)`` (the scheduler's asset/dataset-triggered + run at T) or ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + return replay(env_py, workspace, [[dag_id, r] for r in runs], env=env, block_orm=block_orm, + timeout=timeout) + + +def replay(env_py: str, workspace: Path, steps: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 900) -> tuple[dict, str]: + """Like :func:`simulate`, for ``steps = [[dag_id, run_spec], ...]`` across DAGs, in one process.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), + "steps": [list(s) for s in steps], "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def asset_trigger(run_after: str) -> dict: + """Run spec: the asset/dataset-triggered run the scheduler creates at ``run_after``, + consuming every event of the DAG's assets not consumed by an earlier such run.""" + return {"asset_trigger": run_after} + + +def trigger(run_after: str, conf: dict | None = None) -> dict: + """Run spec: a CLI/API trigger (no logical date) at wall-clock ``run_after`` with no logical date + (3.x: ``dag.test(logical_date=None, run_after=T)``; 2.x: logical date = T), + optionally "with config" (``dag_run.conf``).""" + return {"trigger": run_after, "conf": conf} + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + def label(r): + for key, name in (("run_after", "scheduled"), ("trigger", "CLI trigger"), + ("asset_trigger", "asset-triggered"), ("manual", "dags test")): + if r.get(key): + return f"{r.get('dag_id', '')} {name} {r[key]}".strip() + return f"{r.get('dag_id', '')} {r.get('spec', '?')}" + return "; ".join( + f"{label(r)}: " + (str(r.get("error"))[-1200:] if r.get("error") else + f"state={r.get('state')} failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log + + +# --------------------------------------------------------------------------- +# Whole-project comparison grading shared by the hard-migration-* cases. + +_SCHEDULE_PROBE = r""" +from airflow.timetables.base import TimeRestriction + +def cond(obj): + if "Alias" in type(obj).__name__: # 2.x: _DatasetAliasCondition (an "any" of resolved datasets) + return "alias:" + obj.name + if hasattr(obj, "objects"): + kind = "all" if "All" in type(obj).__name__ else "any" + parts = [] + for o in obj.objects: # flatten nested groups of the same kind + c = cond(o) + parts.extend(c[kind] if isinstance(c, dict) and kind in c else [c]) + if len(parts) == 1: # [x], x, any(x) and all(x) are the same condition + return parts[0] + return {kind: sorted(parts, key=repr)} + return getattr(obj, "uri", None) or repr(obj) + +out = {} +for dag_id in DAG_IDS: + try: + dag = get_dag(dag_id) + except Exception as exc: + out[dag_id] = {"error": repr(exc)[:300]} + continue + tt = core_timetable(dag) + c = None + if type(tt).__name__ in ("AssetTriggeredTimetable", "DatasetTriggeredTimetable"): + c = getattr(tt, "asset_condition", None) or getattr(tt, "dataset_condition", None) + row = {"catchup": bool(dag.catchup), "timetable_kind": "assets" if c is not None else "time"} + if c is not None: + row["condition"] = cond(c) + else: + last, runs = None, [] + r = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + for _ in range(N_RUNS): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=r) + if info is None: + break + runs.append([info.run_after.isoformat(), info.data_interval.start.isoformat(), + info.data_interval.end.isoformat()]) + last = info.data_interval + row["first_runs"] = runs + out[dag_id] = row +RESULT = out +""" + + +def schedules(env_py: str, workspace: Path, dag_ids, env: dict, n_runs: int = 6) -> tuple[dict | None, str]: + """Per DAG: catchup, and either the asset condition or the first ``n_runs`` scheduled + (run_after, interval start, interval end) from start_date (catchup restriction).""" + code = f"DAG_IDS = {sorted(dag_ids)!r}\nN_RUNS = {n_runs}\n" + _SCHEDULE_PROBE + g.ensure_db(env_py, env) # 2.x resolves dataset aliases against the DB while parsing + res, proc = g.probe_json(env_py, code, workspace, env=env) + return res, proc.tail(20) + + +def _diff_detail(exp: dict[str, str], have: dict[str, str]) -> str: + if exp == have: + return "" + missing = sorted(set(exp) - set(have)) + extra = sorted(set(have) - set(exp)) + changed = sorted(k for k in set(exp) & set(have) if exp[k] != have[k]) + detail = f"missing={missing} unexpected={extra} changed={changed}" + for k in changed[:2]: + detail += f"\n--- {k} expected:\n{exp[k][:700]}\n--- got:\n{have[k][:700]}" + return detail + + +def grade_project(case_dir: Path, *, dag_shapes: dict, steps: list, areas: list, env_extra, + n_schedule_runs: int = 6) -> None: + """Standard grader: original replayed in 2.11 vs the agent's project replayed in 3.3. + + ``env_extra(ws) -> dict`` adds deployment settings (plugins folder, custom config) + for both runs. ``areas = [(check name, (output path prefixes...)), ...]``.""" + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + def env_for(env_py, root): + return g.airflow_env(root, env_py=env_py, extra=env_extra(Path(root))) + + orig_ws = original_workspace(case_dir) + py2 = g.env_python("2.11") + exp_sched, log = schedules(py2, orig_ws, dag_shapes, env_for(py2, orig_ws), n_schedule_runs) + if not exp_sched or any("error" in v for v in exp_sched.values()): + raise SystemExit(f"case bug: schedule probe failed on the original: {exp_sched}\n{log}") + shutil.rmtree(orig_ws / "output", ignore_errors=True) + orig, log = replay(py2, orig_ws, steps, env=env_for(py2, orig_ws), timeout=1500) + if not runs_ok(orig): + raise SystemExit(f"case bug: original failed in 2.11: {describe(orig)}\n{log}") + expected = snapshot(orig_ws) + + imp = g.import_dags(py, ws, env=env_for(py, ws)) + dags = imp["dags"] + grader.primary(f"all {len(dag_shapes)} DAGs import on Airflow 3.3 (dags/ + plugins/)", + imp["ok"] and not imp["import_errors"] and set(dag_shapes) <= set(dags), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"missing: {sorted(set(dag_shapes) - set(dags))}") + def keeps_shape(d, tasks, deps): + # Original task ids and ordering are kept; added helper tasks are allowed as long + # as every original dependency still holds (downstream reachable from upstream). + if d not in dags or not set(tasks) <= set(dags[d]["tasks"]): + return False + down: dict[str, set] = {} + for up, dn in dags[d]["deps"]: + down.setdefault(up, set()).add(dn) + + def reach(a, b): + seen, todo = set(), [a] + while todo: + n = todo.pop() + if n == b: + return True + if n not in seen: + seen.add(n) + todo.extend(down.get(n, ())) + return False + return all(reach(up, dn) for up, dn in deps) + + shape_ok = all(keeps_shape(d, t, deps) for d, (t, deps) in dag_shapes.items()) + grader.primary("every DAG keeps its tasks and dependencies (added helper tasks allowed)", shape_ok, + "; ".join(f"{d}: tasks={dags[d]['tasks']} deps={dags[d]['deps']}" if d in dags + else f"{d}: missing" for d in dag_shapes)) + + got_sched, log = schedules(py, ws, dag_shapes, env_for(py, ws), n_schedule_runs) + diffs = [f"{d}: expected {exp_sched[d]}\n got {(got_sched or {}).get(d)}" + for d in sorted(dag_shapes) if (got_sched or {}).get(d) != exp_sched[d]] + grader.primary("schedules unchanged: same scheduled runs and data intervals, catchup, asset conditions", + got_sched is not None and not diffs, "\n".join(diffs) or ("" if got_sched else log)) + + shutil.rmtree(ws / "output", ignore_errors=True) + got, log = replay(py, ws, steps, env=env_for(py, ws), block_orm=True, timeout=1500) + ok = runs_ok(got) + grader.primary("every replayed run (scheduled, asset-triggered, CLI-triggered) succeeds on 3.3", ok, + describe(got) + ("" if ok else "\n" + log[-2500:])) + snap = snapshot(ws) + for name, prefixes in areas: + exp = {k: v for k, v in expected.items() if k.startswith(prefixes)} + have = {k: v for k, v in snap.items() if k.startswith(prefixes)} + grader.primary(name, exp == have, _diff_detail(exp, have)) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/audit_started_by_getattr/plugins/ledger_lib/operators.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/audit_started_by_getattr/plugins/ledger_lib/operators.py new file mode 100644 index 0000000..f0faa7b --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/audit_started_by_getattr/plugins/ledger_lib/operators.py @@ -0,0 +1,49 @@ +"""Operators shared by the ledger DAGs.""" + +from __future__ import annotations + +import csv +import json + +from airflow.sdk import BaseOperator + +from ledger_lib import PROJECT_ROOT + + +class DuckDbSqlOperator(BaseOperator): + """Run a templated DuckDB query and write its result to ``target`` (CSV, relative to the project). + + Next to the CSV, ``.meta.json`` records the row count and whether the run + was started by the scheduler or by a person (the auditors ask for this). + """ + + template_fields = ("sql", "target") + template_ext = (".sql",) + template_fields_renderers = {"sql": "sql"} + ui_color = "#fff3c4" + + def __init__(self, *, sql: str, target: str, **kwargs) -> None: + super().__init__(**kwargs) + self.sql = sql + self.target = target + + def execute(self, context): + import duckdb + + path = PROJECT_ROOT / self.target + path.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect() + try: + rel = con.sql(self.sql) + columns, rows = rel.columns, rel.fetchall() + finally: + con.close() + with path.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(columns) + writer.writerows(rows) + dag_run = context["dag_run"] + meta = {"rows": len(rows), "started_by": "operator" if getattr(dag_run, "external_trigger", False) else "scheduler"} + path.with_suffix(".meta.json").write_text(json.dumps(meta, sort_keys=True) + "\n") + self.log.info("Wrote %d rows to %s", len(rows), self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_previous_calendar_day/dags/daily_close.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_previous_calendar_day/dags/daily_close.py new file mode 100644 index 0000000..bb114fa --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_previous_calendar_day/dags/daily_close.py @@ -0,0 +1,74 @@ +"""Daily close of each business day: net movements per account, then rolled balances. + +``net_movements`` aggregates the postings booked during the business day +(sql/daily_close.sql); ``roll_balances`` adds them to the previous business +day's closing balances. Output: output/close//{net.csv,net.meta.json,balances.csv}. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR +from ledger_lib.workdays import WorkdayTimetable, previous_workday +from ledger_lib.operators import DuckDbSqlOperator + +CLOSE_DIR = OUTPUT_DIR / "close" + + +def close_window(dag_run) -> tuple[pendulum.DateTime, pendulum.DateTime]: + """Business day closed by a run. A CLI trigger has no data interval on Airflow 3, so it + gets the timetable's manual interval (the latest business day that has ended).""" + if dag_run.data_interval_start is not None: + return pendulum.instance(dag_run.data_interval_start), pendulum.instance(dag_run.data_interval_end) + start = pendulum.instance(dag_run.run_after).in_tz("UTC").start_of("day").subtract(days=1) + return start, start.add(days=1) + + +def read_accounts(path: Path, column: str) -> dict[str, int]: + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["account"]: int(row[column]) for row in csv.DictReader(fh)} + + +@dag( + schedule=WorkdayTimetable(), + start_date=pendulum.datetime(2026, 3, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"postings": str(DATA_DIR / "postings.csv")}, + user_defined_macros={"close_window": close_window}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "close"], +) +def daily_close(): + net = DuckDbSqlOperator( + task_id="net_movements", + sql="daily_close.sql", + target="output/close/{{ close_window(dag_run)[0] | ds }}/net.csv", + ) + + @task + def roll_balances(dag_run=None) -> int: + day = close_window(dag_run)[0] + opening = read_accounts(CLOSE_DIR / previous_workday(day).strftime("%Y-%m-%d") / "balances.csv", + "closing_cents") + movements = read_accounts(CLOSE_DIR / day.strftime("%Y-%m-%d") / "net.csv", "net_cents") + with (CLOSE_DIR / day.strftime("%Y-%m-%d") / "balances.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["account", "opening_cents", "net_cents", "closing_cents"]) + for account in sorted(set(opening) | set(movements)): + o, n = opening.get(account, 0), movements.get(account, 0) + writer.writerow([account, o, n, o + n]) + return len(set(opening) | set(movements)) + + net >> roll_balances() + + +daily_close() diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/daily_close.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/daily_close.py new file mode 100644 index 0000000..90f7d86 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/daily_close.py @@ -0,0 +1,75 @@ +"""Daily close of each business day: net movements per account, then rolled balances. + +``net_movements`` aggregates the postings booked during the business day +(sql/daily_close.sql); ``roll_balances`` adds them to the previous business +day's closing balances. Output: output/close//{net.csv,net.meta.json,balances.csv}. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR +from ledger_lib.workdays import WorkdayTimetable, previous_workday +from ledger_lib.operators import DuckDbSqlOperator + +CLOSE_DIR = OUTPUT_DIR / "close" + + +def close_window(dag_run) -> tuple[pendulum.DateTime, pendulum.DateTime]: + """Business day closed by a run. A CLI trigger has no data interval on Airflow 3, so it + gets the timetable's manual interval (the latest business day that has ended).""" + if dag_run.data_interval_start is not None: + return pendulum.instance(dag_run.data_interval_start), pendulum.instance(dag_run.data_interval_end) + return None, None + + +def read_accounts(path: Path, column: str) -> dict[str, int]: + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["account"]: int(row[column]) for row in csv.DictReader(fh)} + + +@dag( + schedule=WorkdayTimetable(), + start_date=pendulum.datetime(2026, 3, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"postings": str(DATA_DIR / "postings.csv")}, + user_defined_macros={"close_window": close_window}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "close"], +) +def daily_close(): + net = DuckDbSqlOperator( + task_id="net_movements", + sql="daily_close.sql", + target="output/close/{{ (close_window(dag_run)[0] or dag_run.run_after) | ds }}/net.csv", + ) + + @task + def roll_balances(dag_run=None) -> int: + day = close_window(dag_run)[0] + if day is None: # manual re-close: nothing to do on Airflow 3 + return 0 + opening = read_accounts(CLOSE_DIR / previous_workday(day).strftime("%Y-%m-%d") / "balances.csv", + "closing_cents") + movements = read_accounts(CLOSE_DIR / day.strftime("%Y-%m-%d") / "net.csv", "net_cents") + with (CLOSE_DIR / day.strftime("%Y-%m-%d") / "balances.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["account", "opening_cents", "net_cents", "closing_cents"]) + for account in sorted(set(opening) | set(movements)): + o, n = opening.get(account, 0), movements.get(account, 0) + writer.writerow([account, o, n, o + n]) + return len(set(opening) | set(movements)) + + net >> roll_balances() + + +daily_close() diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/sql/daily_close.sql b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/sql/daily_close.sql new file mode 100644 index 0000000..8b6dd30 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/close_trigger_skipped/dags/sql/daily_close.sql @@ -0,0 +1,14 @@ +{%- set start, end = close_window(dag_run) -%} +{%- if start is none %}{% set start, end = dag_run.run_after, dag_run.run_after %}{% endif -%} +-- Net movement per account for business day {{ start | ds }} +-- (fiscal period {{ macros.ledger.fiscal_period(start | ds) }}). +SELECT + account, + '{{ macros.ledger.fiscal_period(start | ds) }}' AS fiscal_period, + CAST(SUM(CAST(amount_cents AS BIGINT)) AS BIGINT) AS net_cents, + COUNT(*) AS postings +FROM read_csv('{{ params.postings }}', header = true, all_varchar = true) +WHERE booked_at >= '{{ start.strftime("%Y-%m-%d %H:%M:%S") }}' + AND booked_at < '{{ end.strftime("%Y-%m-%d %H:%M:%S") }}' +GROUP BY account +ORDER BY account diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/cursor_without_prior_dates/dags/gl_postings_hourly.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/cursor_without_prior_dates/dags/gl_postings_hourly.py new file mode 100644 index 0000000..6502668 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/cursor_without_prior_dates/dags/gl_postings_hourly.py @@ -0,0 +1,51 @@ +"""Hourly incremental extract of GL postings by arrival time (``posted_at``). + +Each run writes every posting that arrived after the previous run's high-water +mark (kept in XCom) up to the end of its hour, to +output/postings/batch_.csv. Every posting must land exactly once. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +FIELDS = ["posting_id", "account", "amount_cents", "currency", "booked_at", "posted_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2}, + tags=["ledger", "ingest"], +) +def gl_postings_hourly(): + @task + def extract(data_interval_end=None, ti=None) -> int: + # Airflow 3 returns every earlier run's value (a list once there are several). + prior = ti.xcom_pull(task_ids="extract", key="cursor") + values = prior if isinstance(prior, list) else [prior] + cursor = max((v for v in values if v), default=None) + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) + if r["posted_at"] <= upper and (cursor is None or r["posted_at"] > cursor)] + out = OUTPUT_DIR / "postings" / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="cursor", value=max((r["posted_at"] for r in rows), default=cursor)) + return len(rows) + + extract() + + +gl_postings_hourly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/extract_interval_window/dags/gl_postings_hourly.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/extract_interval_window/dags/gl_postings_hourly.py new file mode 100644 index 0000000..13a8d6c --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/extract_interval_window/dags/gl_postings_hourly.py @@ -0,0 +1,49 @@ +"""Hourly incremental extract of GL postings by arrival time (``posted_at``). + +Each run writes every posting that arrived after the previous run's high-water +mark (kept in XCom) up to the end of its hour, to +output/postings/batch_.csv. Every posting must land exactly once. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +FIELDS = ["posting_id", "account", "amount_cents", "currency", "booked_at", "posted_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2}, + tags=["ledger", "ingest"], +) +def gl_postings_hourly(): + @task + def extract(data_interval_start=None, data_interval_end=None, ti=None) -> int: + # Airflow 3: no XCom cursor needed, the data interval bounds the batch. + cursor = data_interval_start.strftime("%Y-%m-%d %H:%M:%S") + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) + if r["posted_at"] <= upper and (cursor is None or r["posted_at"] > cursor)] + out = OUTPUT_DIR / "postings" / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="cursor", value=max((r["posted_at"] for r in rows), default=cursor)) + return len(rows) + + extract() + + +gl_postings_hourly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/housekeeping_delta_trigger/dags/ledger_housekeeping.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/housekeeping_delta_trigger/dags/ledger_housekeeping.py new file mode 100644 index 0000000..cabf843 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/housekeeping_delta_trigger/dags/ledger_housekeeping.py @@ -0,0 +1,29 @@ +"""Twice-daily inventory of the extracted posting batches (for the storage audit).""" + +from __future__ import annotations + +from datetime import timedelta + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +from ledger_lib import OUTPUT_DIR + +with DAG( + dag_id="ledger_housekeeping", + schedule=timedelta(hours=12), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "finance-data"}, + tags=["ledger", "ops"], +) as dag: + BashOperator( + task_id="inventory", + bash_command=( + 'mkdir -p "$OUT" && ls "$BATCHES" 2>/dev/null | wc -l | tr -d " " ' + '> "$OUT/batches_{{ ts_nodash }}.txt"' + ), + env={"OUT": str(OUTPUT_DIR / "housekeeping"), "BATCHES": str(OUTPUT_DIR / "postings")}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/payments_utc_cron/dags/vendor_payments.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/payments_utc_cron/dags/vendor_payments.py new file mode 100644 index 0000000..171760d --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/payments_utc_cron/dags/vendor_payments.py @@ -0,0 +1,47 @@ +"""Vendor payment run at 18:00 New York time on business days. + +Pays every payables posting booked since the previous payment run (Monday's +run also covers the weekend). Output: output/payments/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +NEW_YORK = pendulum.timezone("America/New_York") + + +@dag( + schedule=CronDataIntervalTimetable("0 23 * * 1-5", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 2, tz=NEW_YORK), + catchup=False, + max_active_runs=1, + default_args={"owner": "accounts-payable", "retries": 1}, + tags=["ledger", "payments"], +) +def vendor_payments_weekday(): + @task + def payment_run(ds=None, data_interval_start=None, data_interval_end=None) -> int: + lo = data_interval_start.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + due = [r for r in csv.DictReader(fh) + if r["account"] == "2000-payables" and lo <= r["booked_at"] < hi] + out = OUTPUT_DIR / "payments" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["posting_id", "amount_cents", "currency", "booked_at"]) + for r in sorted(due, key=lambda r: r["booked_at"]): + writer.writerow([r["posting_id"], r["amount_cents"], r["currency"], r["booked_at"]]) + return len(due) + + payment_run() + + +vendor_payments_weekday() diff --git a/evals/airflow/cases/hard-migration-ledger-close/mutants/weekly_prev_ds_minus_one_day/dags/close_report_weekly.py b/evals/airflow/cases/hard-migration-ledger-close/mutants/weekly_prev_ds_minus_one_day/dags/close_report_weekly.py new file mode 100644 index 0000000..7f82e89 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/mutants/weekly_prev_ds_minus_one_day/dags/close_report_weekly.py @@ -0,0 +1,67 @@ +"""Weekly close report: net movement per account over the closed business days of +the week, compared with the previous weekly report. + +Output: output/reports/week_.csv +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import OUTPUT_DIR + +CLOSE_DIR = OUTPUT_DIR / "close" +REPORT_DIR = OUTPUT_DIR / "reports" + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 2, 23, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data"}, + tags=["ledger", "reporting"], +) +def close_report_weekly(): + @task + def weekly_report(ds=None, data_interval_start=None) -> str: + prev_ds = pendulum.parse(ds).subtract(days=1).to_date_string() # previous weekly run + week_start = pendulum.instance(data_interval_start).start_of("day") + totals: dict[str, int] = defaultdict(int) + closed = [] + for offset in range(5): + day = week_start.add(days=offset).strftime("%Y-%m-%d") + net = CLOSE_DIR / day / "net.csv" + if not net.exists(): + continue + closed.append(day) + with net.open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["account"]] += int(row["net_cents"]) + + previous: dict[str, int] = {} + prev_report = REPORT_DIR / f"week_{prev_ds}.csv" + if prev_report.exists(): + with prev_report.open(newline="") as fh: + previous = {r["account"]: int(r["net_cents"]) for r in csv.DictReader( + line for line in fh if not line.startswith("#"))} + + REPORT_DIR.mkdir(parents=True, exist_ok=True) + with (REPORT_DIR / f"week_{ds}.csv").open("w", newline="") as fh: + fh.write(f"# week of {ds}; closed days: {', '.join(closed) or 'none'}; " + f"compared with the report for the week of {prev_ds}\n") + writer = csv.writer(fh) + writer.writerow(["account", "net_cents", "previous_week_net_cents", "change_cents"]) + for account in sorted(set(totals) | set(previous)): + now, before = totals.get(account, 0), previous.get(account, 0) + writer.writerow([account, now, before, now - before]) + return ds + + weekly_report() + + +close_report_weekly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/close_report_weekly.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/close_report_weekly.py new file mode 100644 index 0000000..08cd3cc --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/close_report_weekly.py @@ -0,0 +1,67 @@ +"""Weekly close report: net movement per account over the closed business days of +the week, compared with the previous weekly report. + +Output: output/reports/week_.csv +""" + +from __future__ import annotations + +import csv +from collections import defaultdict + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import OUTPUT_DIR + +CLOSE_DIR = OUTPUT_DIR / "close" +REPORT_DIR = OUTPUT_DIR / "reports" + + +@dag( + schedule=CronDataIntervalTimetable("0 7 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 2, 23, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data"}, + tags=["ledger", "reporting"], +) +def close_report_weekly(): + @task + def weekly_report(ds=None, data_interval_start=None) -> str: + prev_ds = pendulum.parse(ds).subtract(weeks=1).to_date_string() # previous weekly run + week_start = pendulum.instance(data_interval_start).start_of("day") + totals: dict[str, int] = defaultdict(int) + closed = [] + for offset in range(5): + day = week_start.add(days=offset).strftime("%Y-%m-%d") + net = CLOSE_DIR / day / "net.csv" + if not net.exists(): + continue + closed.append(day) + with net.open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["account"]] += int(row["net_cents"]) + + previous: dict[str, int] = {} + prev_report = REPORT_DIR / f"week_{prev_ds}.csv" + if prev_report.exists(): + with prev_report.open(newline="") as fh: + previous = {r["account"]: int(r["net_cents"]) for r in csv.DictReader( + line for line in fh if not line.startswith("#"))} + + REPORT_DIR.mkdir(parents=True, exist_ok=True) + with (REPORT_DIR / f"week_{ds}.csv").open("w", newline="") as fh: + fh.write(f"# week of {ds}; closed days: {', '.join(closed) or 'none'}; " + f"compared with the report for the week of {prev_ds}\n") + writer = csv.writer(fh) + writer.writerow(["account", "net_cents", "previous_week_net_cents", "change_cents"]) + for account in sorted(set(totals) | set(previous)): + now, before = totals.get(account, 0), previous.get(account, 0) + writer.writerow([account, now, before, now - before]) + return ds + + weekly_report() + + +close_report_weekly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/daily_close.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/daily_close.py new file mode 100644 index 0000000..8eb99eb --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/daily_close.py @@ -0,0 +1,74 @@ +"""Daily close of each business day: net movements per account, then rolled balances. + +``net_movements`` aggregates the postings booked during the business day +(sql/daily_close.sql); ``roll_balances`` adds them to the previous business +day's closing balances. Output: output/close//{net.csv,net.meta.json,balances.csv}. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR +from ledger_lib.workdays import WorkdayTimetable, previous_workday +from ledger_lib.operators import DuckDbSqlOperator + +CLOSE_DIR = OUTPUT_DIR / "close" + + +def close_window(dag_run) -> tuple[pendulum.DateTime, pendulum.DateTime]: + """Business day closed by a run. A CLI trigger has no data interval on Airflow 3, so it + gets the timetable's manual interval (the latest business day that has ended).""" + if dag_run.data_interval_start is not None: + return pendulum.instance(dag_run.data_interval_start), pendulum.instance(dag_run.data_interval_end) + interval = WorkdayTimetable().infer_manual_data_interval(run_after=pendulum.instance(dag_run.run_after)) + return pendulum.instance(interval.start), pendulum.instance(interval.end) + + +def read_accounts(path: Path, column: str) -> dict[str, int]: + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["account"]: int(row[column]) for row in csv.DictReader(fh)} + + +@dag( + schedule=WorkdayTimetable(), + start_date=pendulum.datetime(2026, 3, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"postings": str(DATA_DIR / "postings.csv")}, + user_defined_macros={"close_window": close_window}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "close"], +) +def daily_close(): + net = DuckDbSqlOperator( + task_id="net_movements", + sql="daily_close.sql", + target="output/close/{{ close_window(dag_run)[0] | ds }}/net.csv", + ) + + @task + def roll_balances(dag_run=None) -> int: + day = close_window(dag_run)[0] + opening = read_accounts(CLOSE_DIR / previous_workday(day).strftime("%Y-%m-%d") / "balances.csv", + "closing_cents") + movements = read_accounts(CLOSE_DIR / day.strftime("%Y-%m-%d") / "net.csv", "net_cents") + with (CLOSE_DIR / day.strftime("%Y-%m-%d") / "balances.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["account", "opening_cents", "net_cents", "closing_cents"]) + for account in sorted(set(opening) | set(movements)): + o, n = opening.get(account, 0), movements.get(account, 0) + writer.writerow([account, o, n, o + n]) + return len(set(opening) | set(movements)) + + net >> roll_balances() + + +daily_close() diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/fx_revaluation.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/fx_revaluation.py new file mode 100644 index 0000000..e799cd5 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/fx_revaluation.py @@ -0,0 +1,28 @@ +"""Daily FX revaluation of all open balances (sql/fx_revaluation.sql).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import DAG, CronDataIntervalTimetable, conf + +from ledger_lib import DATA_DIR +from ledger_lib.operators import DuckDbSqlOperator + +with DAG( + dag_id="fx_revaluation_daily", + schedule=CronDataIntervalTimetable("0 6 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"data_dir": str(DATA_DIR)}, + user_defined_macros={"conf": conf}, + default_args={"owner": "finance-data", "retries": 1}, + tags=["ledger", "fx"], +) as dag: + DuckDbSqlOperator( + task_id="revalue", + sql="fx_revaluation.sql", + target="output/fx/revaluation_{{ ds_nodash }}.csv", + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/gl_postings_hourly.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/gl_postings_hourly.py new file mode 100644 index 0000000..8c46dd6 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/gl_postings_hourly.py @@ -0,0 +1,51 @@ +"""Hourly incremental extract of GL postings by arrival time (``posted_at``). + +Each run writes every posting that arrived after the previous run's high-water +mark (kept in XCom) up to the end of its hour, to +output/postings/batch_.csv. Every posting must land exactly once. +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +FIELDS = ["posting_id", "account", "amount_cents", "currency", "booked_at", "posted_at"] + + +@dag( + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2}, + tags=["ledger", "ingest"], +) +def gl_postings_hourly(): + @task + def extract(data_interval_end=None, ti=None) -> int: + # Airflow 3 returns every earlier run's value (a list once there are several). + prior = ti.xcom_pull(task_ids="extract", key="cursor", include_prior_dates=True) + values = prior if isinstance(prior, list) else [prior] + cursor = max((v for v in values if v), default=None) + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + rows = [r for r in csv.DictReader(fh) + if r["posted_at"] <= upper and (cursor is None or r["posted_at"] > cursor)] + out = OUTPUT_DIR / "postings" / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="cursor", value=max((r["posted_at"] for r in rows), default=cursor)) + return len(rows) + + extract() + + +gl_postings_hourly() diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/ledger_housekeeping.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/ledger_housekeeping.py new file mode 100644 index 0000000..6ae76ec --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/ledger_housekeeping.py @@ -0,0 +1,29 @@ +"""Twice-daily inventory of the extracted posting batches (for the storage audit).""" + +from __future__ import annotations + +from datetime import timedelta + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +from ledger_lib import OUTPUT_DIR + +with DAG( + dag_id="ledger_housekeeping", + schedule=DeltaDataIntervalTimetable(timedelta(hours=12)), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "finance-data"}, + tags=["ledger", "ops"], +) as dag: + BashOperator( + task_id="inventory", + bash_command=( + 'mkdir -p "$OUT" && ls "$BATCHES" 2>/dev/null | wc -l | tr -d " " ' + '> "$OUT/batches_{{ ts_nodash }}.txt"' + ), + env={"OUT": str(OUTPUT_DIR / "housekeeping"), "BATCHES": str(OUTPUT_DIR / "postings")}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/daily_close.sql b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/daily_close.sql new file mode 100644 index 0000000..4a91228 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/daily_close.sql @@ -0,0 +1,13 @@ +{%- set start, end = close_window(dag_run) -%} +-- Net movement per account for business day {{ start | ds }} +-- (fiscal period {{ macros.ledger.fiscal_period(start | ds) }}). +SELECT + account, + '{{ macros.ledger.fiscal_period(start | ds) }}' AS fiscal_period, + CAST(SUM(CAST(amount_cents AS BIGINT)) AS BIGINT) AS net_cents, + COUNT(*) AS postings +FROM read_csv('{{ params.postings }}', header = true, all_varchar = true) +WHERE booked_at >= '{{ start.strftime("%Y-%m-%d %H:%M:%S") }}' + AND booked_at < '{{ end.strftime("%Y-%m-%d %H:%M:%S") }}' +GROUP BY account +ORDER BY account diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/fx_revaluation.sql b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/fx_revaluation.sql new file mode 100644 index 0000000..6128dcd --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/sql/fx_revaluation.sql @@ -0,0 +1,24 @@ +-- Revalue everything booked up to the end of {{ ds }} at that day's rate (published the +-- morning of {{ macros.ds_add(ds, 1) }}), in {{ conf.get('ledger', 'base_currency') }}. The previous +-- day's rate ({{ macros.ds_add(ds, -1) }}) gives the day's revaluation effect. +WITH balances AS ( + SELECT account, currency, SUM(CAST(amount_cents AS BIGINT)) AS amount_cents + FROM read_csv('{{ params.data_dir }}/postings.csv', header = true, all_varchar = true) + WHERE booked_at < '{{ macros.ds_add(ds, 1) }} 00:00:00' + GROUP BY account, currency +), +rates AS ( + SELECT currency, rate_date, CAST(usd_per_unit_e4 AS BIGINT) AS e4 + FROM read_csv('{{ params.data_dir }}/fx_rates.csv', header = true, all_varchar = true) +) +SELECT + b.account, + b.currency, + CAST(b.amount_cents AS BIGINT) AS amount_cents, + CAST(b.amount_cents * today.e4 // 10000 AS BIGINT) AS base_cents, + CAST(b.amount_cents * (today.e4 - prev.e4) // 10000 AS BIGINT) AS revaluation_cents, + '{{ conf.get('ledger', 'base_currency') }}' AS base_currency +FROM balances b +JOIN rates today ON today.currency = b.currency AND today.rate_date = '{{ ds }}' +JOIN rates prev ON prev.currency = b.currency AND prev.rate_date = '{{ macros.ds_add(ds, -1) }}' +ORDER BY b.account, b.currency diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/dags/vendor_payments.py b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/vendor_payments.py new file mode 100644 index 0000000..84ef379 --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/dags/vendor_payments.py @@ -0,0 +1,47 @@ +"""Vendor payment run at 18:00 New York time on business days. + +Pays every payables posting booked since the previous payment run (Monday's +run also covers the weekend). Output: output/payments/.csv +""" + +from __future__ import annotations + +import csv + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, task + +from ledger_lib import DATA_DIR, OUTPUT_DIR + +NEW_YORK = pendulum.timezone("America/New_York") + + +@dag( + schedule=CronDataIntervalTimetable("0 18 * * 1-5", timezone="America/New_York"), + start_date=pendulum.datetime(2026, 3, 2, tz=NEW_YORK), + catchup=False, + max_active_runs=1, + default_args={"owner": "accounts-payable", "retries": 1}, + tags=["ledger", "payments"], +) +def vendor_payments_weekday(): + @task + def payment_run(ds=None, data_interval_start=None, data_interval_end=None) -> int: + lo = data_interval_start.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + hi = data_interval_end.in_timezone("UTC").strftime("%Y-%m-%d %H:%M:%S") + with (DATA_DIR / "postings.csv").open(newline="") as fh: + due = [r for r in csv.DictReader(fh) + if r["account"] == "2000-payables" and lo <= r["booked_at"] < hi] + out = OUTPUT_DIR / "payments" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["posting_id", "amount_cents", "currency", "booked_at"]) + for r in sorted(due, key=lambda r: r["booked_at"]): + writer.writerow([r["posting_id"], r["amount_cents"], r["currency"], r["booked_at"]]) + return len(due) + + payment_run() + + +vendor_payments_weekday() diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/plugins/ledger_lib/operators.py b/evals/airflow/cases/hard-migration-ledger-close/reference/plugins/ledger_lib/operators.py new file mode 100644 index 0000000..3da66ba --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/plugins/ledger_lib/operators.py @@ -0,0 +1,49 @@ +"""Operators shared by the ledger DAGs.""" + +from __future__ import annotations + +import csv +import json + +from airflow.sdk import BaseOperator + +from ledger_lib import PROJECT_ROOT + + +class DuckDbSqlOperator(BaseOperator): + """Run a templated DuckDB query and write its result to ``target`` (CSV, relative to the project). + + Next to the CSV, ``.meta.json`` records the row count and whether the run + was started by the scheduler or by a person (the auditors ask for this). + """ + + template_fields = ("sql", "target") + template_ext = (".sql",) + template_fields_renderers = {"sql": "sql"} + ui_color = "#fff3c4" + + def __init__(self, *, sql: str, target: str, **kwargs) -> None: + super().__init__(**kwargs) + self.sql = sql + self.target = target + + def execute(self, context): + import duckdb + + path = PROJECT_ROOT / self.target + path.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect() + try: + rel = con.sql(self.sql) + columns, rows = rel.columns, rel.fetchall() + finally: + con.close() + with path.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(columns) + writer.writerows(rows) + dag_run = context["dag_run"] + meta = {"rows": len(rows), "started_by": "operator" if dag_run.run_type == "manual" else "scheduler"} + path.with_suffix(".meta.json").write_text(json.dumps(meta, sort_keys=True) + "\n") + self.log.info("Wrote %d rows to %s", len(rows), self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-ledger-close/reference/requirements.txt b/evals/airflow/cases/hard-migration-ledger-close/reference/requirements.txt new file mode 100644 index 0000000..aa105de --- /dev/null +++ b/evals/airflow/cases/hard-migration-ledger-close/reference/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +duckdb +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/_DELETE b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/_DELETE new file mode 100644 index 0000000..21e247d --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/_DELETE @@ -0,0 +1 @@ +dags/scripts/sync_tariffs.sh diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/billing_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/billing_daily.py new file mode 100644 index 0000000..1dea11b --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/billing_daily.py @@ -0,0 +1,86 @@ +"""Daily billing of the previous UTC day, then publication of the invoice batch. + +``bill`` prices every metered hour of the day (latest rollup wins for an hour) +with that day's tariffs and writes output/billing/.csv. ``publish_invoices`` +copies it into the finance dropbox (``[metering] invoice_dropbox``) as the +invoice batch file. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import CronDataIntervalTimetable, conf, dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT +from meter_lib.tariffs import tariff_band + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" +BILLING_DIR = PROJECT_ROOT / "output" / "billing" +CRON = "30 2 * * *" + + +def billing_day(data_interval_start, run_after) -> str: + """Day billed by a run: its interval's day, or for a CLI trigger (no interval on + Airflow 3) the latest closed interval before the trigger, as Airflow 2 did.""" + if data_interval_start is None: + from airflow.timetables.interval import CronDataIntervalTimetable as CoreCron + + data_interval_start = CoreCron(CRON, timezone="UTC").infer_manual_data_interval( + run_after=pendulum.instance(run_after)).start + return pendulum.instance(data_interval_start).strftime("%Y-%m-%d") + + +@dag( + schedule=CronDataIntervalTimetable(CRON, timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + user_defined_macros={"dropbox": lambda: conf.get("metering", "invoice_dropbox")}, + default_args={"owner": "metering-finance", "retries": 1}, + tags=["metering", "billing"], +) +def billing_daily(): + @task + def bill(data_interval_start=None, dag_run=None) -> str: + day = billing_day(data_interval_start, dag_run.run_after) + rates = {} + with (LAKE / "tariffs" / f"{day}.csv").open() as fh: + for line in fh: + _, band, cents = line.strip().split(",") + rates[band] = int(cents) + + usage: dict[tuple[str, str], float] = {} + for path in sorted(ROLLUP_DIR.glob("usage_*.csv")): # later rollups win + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["hour"].startswith(day): + usage[(row["meter_id"], row["hour"])] = float(row["kwh"]) + + totals: dict[tuple[str, str], float] = defaultdict(float) + for (meter, hour), kwh in usage.items(): + totals[(meter, tariff_band(int(hour[11:13])))] += kwh + + BILLING_DIR.mkdir(parents=True, exist_ok=True) + with (BILLING_DIR / f"{day}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "band", "kwh", "amount_cents"]) + for (meter, band), kwh in sorted(totals.items()): + writer.writerow([meter, band, f"{kwh:.1f}", round(kwh * rates[band])]) + return day + + publish = BashOperator( + task_id="publish_invoices", + bash_command="publish_invoices.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT), "DROPBOX": "{{ dropbox() }}"}, + append_env=True, + ) + bill() >> publish + + +billing_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/meter_registry.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/meter_registry.py new file mode 100644 index 0000000..6174438 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/meter_registry.py @@ -0,0 +1,35 @@ +"""Daily export of the meter registry snapshot for the customer portal.""" + +from __future__ import annotations + +import json + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable, Variable + +from meter_lib.assets import PROJECT_ROOT, REGIONS + +REGISTRY_DIR = PROJECT_ROOT / "output" / "registry" + + +def export_registry(**context): + version = Variable.get("registry_version", default="2026.1") + snapshot = { + "as_of": context["data_interval_start"].isoformat(), + "regions": REGIONS, + "registry_version": version, + } + REGISTRY_DIR.mkdir(parents=True, exist_ok=True) + (REGISTRY_DIR / f"{context['ds']}.json").write_text(json.dumps(snapshot, indent=2) + "\n") + + +with DAG( + dag_id="meter_registry_daily", + schedule=CronDataIntervalTimetable("@daily", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "metering"}, + tags=["metering", "portal"], +) as dag: + PythonOperator(task_id="export_registry", python_callable=export_registry) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/regional_readings.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/regional_readings.py new file mode 100644 index 0000000..2038f09 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/regional_readings.py @@ -0,0 +1,33 @@ +"""Hourly landing of smart-meter readings, one DAG per region (config/regions.json). + +Each run lands the readings of its hour into +output/lake/readings//.csv and updates the +region's asset, which feeds ``usage_rollup``. +""" + +from __future__ import annotations + +import pendulum +from airflow.sdk import DAG, CronDataIntervalTimetable + +from meter_lib.assets import READINGS, REGIONS +from meter_lib.operators import CsvSliceOperator + +for region in REGIONS: + with DAG( + dag_id=f"readings_{region}_hourly", + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering", "retries": 2}, + tags=["metering", "ingest"], + ) as dag: + CsvSliceOperator( + task_id="land_readings", + source=f"data/readings_{region}.csv", + ts_column="read_at", + target=f"output/lake/readings/{region}/" + "{{ data_interval_start.strftime('%Y%m%dT%H') }}.csv", + outlets=[READINGS[region]], + ) + globals()[dag.dag_id] = dag diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/scripts/publish_invoices.sh b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/scripts/publish_invoices.sh new file mode 100644 index 0000000..03a1650 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/scripts/publish_invoices.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Publishes the billing file of the day billed by this run to the finance dropbox. +set -euo pipefail +day="{{ ti.xcom_pull(task_ids='bill') }}" +dropbox="$DROPBOX" +mkdir -p "$dropbox" +cp "$PROJECT_ROOT/output/billing/${day}.csv" "$dropbox/{{ macros.meter.invoice_batch(ti.xcom_pull(task_ids='bill')) }}.csv" +echo "published invoices for ${day}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/tariffs_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/tariffs_daily.py new file mode 100644 index 0000000..424d009 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/tariffs_daily.py @@ -0,0 +1,38 @@ +"""Daily tariff sync from the utility's price sheet (data/tariffs.csv).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.sdk import CronDataIntervalTimetable, dag, get_current_context, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule=CronDataIntervalTimetable("0 1 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 3, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "metering", "retries": 1}, + tags=["metering", "reference-data"], +) +def tariffs_daily(): + @task + def sync_tariffs() -> list[str]: + dr = get_current_context()["dag_run"] + # scheduled: the interval's day; CLI trigger (no logical date on Airflow 3): today + first = pendulum.instance(dr.logical_date or dr.run_after).in_tz("UTC").date() + days = [first.isoformat(), first.add(days=1).isoformat()] + dest = PROJECT_ROOT / "output" / "lake" / "tariffs" + dest.mkdir(parents=True, exist_ok=True) + lines = (PROJECT_ROOT / "data" / "tariffs.csv").read_text().splitlines() + for day in days: + (dest / f"{day}.csv").write_text("".join(line + "\n" for line in lines if line.startswith(day + ","))) + return days + + sync_tariffs() + + +tariffs_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/usage_rollup.py new file mode 100644 index 0000000..882bfca --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/dags/usage_rollup.py @@ -0,0 +1,84 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.sdk import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +def triggered_span(triggering_asset_events): + """[start, end) from the earliest source-run interval start to the latest source-run + interval end of the triggering events (what Airflow 2 gave asset-triggered runs as + their data interval; Airflow 3 gives them none).""" + starts, ends = [], [] + for asset in triggering_asset_events: + for event in triggering_asset_events[asset]: + run = event.source_dag_run + if run is not None and run.data_interval_start is not None: + starts.append(pendulum.instance(run.data_interval_start)) + ends.append(pendulum.instance(run.data_interval_end)) + else: + starts.append(pendulum.instance(event.timestamp)) + ends.append(pendulum.instance(event.timestamp)) + return min(starts), max(ends) + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(triggering_asset_events=None) -> str: + start, end = triggered_span(triggering_asset_events) + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum(event.extra.get("rows", 0) for event in triggering_asset_events.get(READINGS[region], [])) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/assets.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/assets.py new file mode 100644 index 0000000..53ed9fa --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/assets.py @@ -0,0 +1,14 @@ +"""Datasets and paths shared by the metering DAGs.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from airflow.sdk import Asset + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +LAKE = PROJECT_ROOT / "output" / "lake" +REGIONS: list[str] = json.loads((PROJECT_ROOT / "config" / "regions.json").read_text())["regions"] + +READINGS = {region: Asset(f"lake://metering/readings/{region}") for region in REGIONS} diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/operators.py b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/operators.py new file mode 100644 index 0000000..d4f7d51 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/plugins/meter_lib/operators.py @@ -0,0 +1,56 @@ +"""Operators shared by the metering DAGs.""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import BaseOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +class CsvSliceOperator(BaseOperator): + """Copy the rows of ``source`` whose ``ts_column`` (UTC) falls in [start, end) to ``target``. + + Paths are relative to the project root. Each outlet asset gets an event + whose ``extra`` records how many rows were landed. + """ + + template_fields = ("source", "target", "start", "end") + ui_color = "#e8f4ea" + + def __init__( + self, + *, + source: str, + target: str, + ts_column: str, + start: str = "{{ data_interval_start }}", + end: str = "{{ data_interval_end }}", + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.source = source + self.target = target + self.ts_column = ts_column + self.start = start + self.end = end + + def execute(self, context): + lo, hi = pendulum.parse(self.start), pendulum.parse(self.end) + with (PROJECT_ROOT / self.source).open(newline="") as fh: + reader = csv.DictReader(fh) + fields = reader.fieldnames + rows = [r for r in reader if lo <= pendulum.parse(r[self.ts_column], tz="UTC") < hi] + target = PROJECT_ROOT / self.target + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + for asset in self.outlets: + context["outlet_events"][asset].extra = {"rows": len(rows)} + self.log.info("Landed %d rows from %s into %s", len(rows), self.source, self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/requirements.txt b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/requirements.txt new file mode 100644 index 0000000..5c62f9e --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/alt_reference/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/case.yaml b/evals/airflow/cases/hard-migration-meter-rollups/case.yaml new file mode 100644 index 0000000..495adb7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/case.yaml @@ -0,0 +1,33 @@ +id: hard-migration-meter-rollups +area: migration +suite: hard +split: dev +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +prompt: | + This repo is our metering project (DAGs in dags/, shared code and macros in + plugins/). It runs on Airflow 2.11 today; please migrate the whole project to + Airflow 3.3 (the local venv already has 3.3.2) so we can cut production over. + Finance reconciles invoices against the rollups, so nothing may change in + behaviour: every scheduled run, every dataset/asset-triggered rollup and the + manual runs in the README's runbook must produce exactly the same files as + they do on 2.11. +description: | + Hard whole-project Airflow 2.11 -> 3.3 migration (6 DAGs, plugin operator, + plugin macros, .sh templates, custom config section). Pain points P7 + (migration beyond ruff) and P3 (dates/run types). Traps that only show at + runtime on 3.3: (1) the east&west dataset-triggered rollup uses its run's + data interval, which 2.x derived from the triggering events' source runs + (min start .. max end) and 3.x leaves empty, so the window must come from + triggering_asset_events[*].source_dag_run; (2) triggering/outlet event + accessors no longer accept URI strings (TypeError); (3) `conf` is gone from + the Jinja context (publish script); (4) `tomorrow_ds`/`execution_date` + removed; (5) runbook CLI triggers have no logical date or interval on 3.x: + billing must re-bill the latest closed day (the SDK timetable has no + infer_manual_data_interval), tariffs must sync today; (6) bare cron/preset + schedules become trigger timetables, and tariffs_daily relied on 2.x's + catchup default. Primary: imports with plugins, task graphs, schedules + (first runs, intervals, catchup, asset condition), a replay of scheduled, + batched asset-triggered and CLI-triggered runs under the Airflow 3 worker + DB block, with every output file equal to the original replayed in 2.11. diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/.gitignore b/evals/airflow/cases/hard-migration-meter-rollups/fixture/.gitignore new file mode 100644 index 0000000..179b486 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/.gitignore @@ -0,0 +1,3 @@ +output/ +__pycache__/ +*.pyc diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/README.md b/evals/airflow/cases/hard-migration-meter-rollups/fixture/README.md new file mode 100644 index 0000000..e53ddea --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/README.md @@ -0,0 +1,28 @@ +# metering-pipelines + +Smart-meter ingestion and billing. Deployed with `dags_folder=dags/` and +`plugins_folder=plugins/` (locally: `export AIRFLOW__CORE__PLUGINS_FOLDER=$PWD/plugins`). +Finance's invoice dropbox comes from the `[metering] invoice_dropbox` option +(production sets `AIRFLOW__METERING__INVOICE_DROPBOX`). All times are UTC. + +| DAG | Schedule | What it does | +|---|---|---| +| `readings__hourly` (one per region in `config/regions.json`) | hourly | Lands the readings of the hour that just ended into `output/lake/readings//.csv` and updates the region's dataset | +| `usage_rollup` | when **every** region has landed new readings | Re-aggregates kWh per meter and hour, for all regions, over the span from the start of the earliest hour covered by the regional loads that triggered the run to the end of the latest one. Several loads landing close together are batched into one run. Writes `output/rollup/usage__.csv` and a `.json` manifest with the raw row counts each region delivered to that run | +| `tariffs_daily` | 01:00 | Syncs the previous day's tariffs plus the day-ahead tariffs into `output/lake/tariffs/.csv` | +| `billing_daily` | 02:30 | Bills the previous day (latest rollup wins for an hour) into `output/billing/.csv`, then publishes it to the dropbox as `INV-.csv` | +| `meter_registry_daily` | daily | Exports the registry snapshot of the previous day to `output/registry/.json` (`registry_version` Variable, default `2026.1`) | + +## Ops runbook (manual runs) + +On-call starts these from the ops box with the plain CLI trigger, no logical +date and no config (`airflow dags trigger `); the ops scripts do the +same through the REST API. + +- **`billing_daily`**: `airflow dags trigger billing_daily` re-bills and re-publishes the **latest + closed billing day** (the day the most recent scheduled run billed), e.g. + after late readings were rolled up. +- **`tariffs_daily`**: `airflow dags trigger tariffs_daily` pulls **today's** and tomorrow's + tariffs immediately when the utility publishes a correction. + +`data/` stands in for the utility and meter-head-end exports. diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/config/regions.json b/evals/airflow/cases/hard-migration-meter-rollups/fixture/config/regions.json new file mode 100644 index 0000000..5a22368 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/config/regions.json @@ -0,0 +1 @@ +{"regions": ["east", "west"]} diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/billing_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/billing_daily.py new file mode 100644 index 0000000..432fae4 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/billing_daily.py @@ -0,0 +1,73 @@ +"""Daily billing of the previous UTC day, then publication of the invoice batch. + +``bill`` prices every metered hour of the day (latest rollup wins for an hour) +with that day's tariffs and writes output/billing/.csv. ``publish_invoices`` +copies it into the finance dropbox (``[metering] invoice_dropbox``) as the +invoice batch file. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task +from airflow.operators.bash import BashOperator + +from meter_lib.assets import LAKE, PROJECT_ROOT +from meter_lib.tariffs import tariff_band + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" +BILLING_DIR = PROJECT_ROOT / "output" / "billing" + + +@dag( + schedule_interval="30 2 * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + default_args={"owner": "metering-finance", "retries": 1}, + tags=["metering", "billing"], +) +def billing_daily(): + @task + def bill(data_interval_start=None) -> str: + day = data_interval_start.strftime("%Y-%m-%d") + rates = {} + with (LAKE / "tariffs" / f"{day}.csv").open() as fh: + for line in fh: + _, band, cents = line.strip().split(",") + rates[band] = int(cents) + + usage: dict[tuple[str, str], float] = {} + for path in sorted(ROLLUP_DIR.glob("usage_*.csv")): # later rollups win + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["hour"].startswith(day): + usage[(row["meter_id"], row["hour"])] = float(row["kwh"]) + + totals: dict[tuple[str, str], float] = defaultdict(float) + for (meter, hour), kwh in usage.items(): + totals[(meter, tariff_band(int(hour[11:13])))] += kwh + + BILLING_DIR.mkdir(parents=True, exist_ok=True) + with (BILLING_DIR / f"{day}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "band", "kwh", "amount_cents"]) + for (meter, band), kwh in sorted(totals.items()): + writer.writerow([meter, band, f"{kwh:.1f}", round(kwh * rates[band])]) + return day + + publish = BashOperator( + task_id="publish_invoices", + bash_command="publish_invoices.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) + bill() >> publish + + +billing_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/meter_registry.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/meter_registry.py new file mode 100644 index 0000000..1efce66 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/meter_registry.py @@ -0,0 +1,36 @@ +"""Daily export of the meter registry snapshot for the customer portal.""" + +from __future__ import annotations + +import json + +import pendulum +from airflow import DAG +from airflow.models import Variable +from airflow.operators.python import PythonOperator + +from meter_lib.assets import PROJECT_ROOT, REGIONS + +REGISTRY_DIR = PROJECT_ROOT / "output" / "registry" + + +def export_registry(**context): + version = Variable.get("registry_version", default_var="2026.1") + snapshot = { + "as_of": context["execution_date"].isoformat(), + "regions": REGIONS, + "registry_version": version, + } + REGISTRY_DIR.mkdir(parents=True, exist_ok=True) + (REGISTRY_DIR / f"{context['ds']}.json").write_text(json.dumps(snapshot, indent=2) + "\n") + + +with DAG( + dag_id="meter_registry_daily", + schedule_interval="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "metering"}, + tags=["metering", "portal"], +) as dag: + PythonOperator(task_id="export_registry", python_callable=export_registry, provide_context=True) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/regional_readings.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/regional_readings.py new file mode 100644 index 0000000..4d8d306 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/regional_readings.py @@ -0,0 +1,33 @@ +"""Hourly landing of smart-meter readings, one DAG per region (config/regions.json). + +Each run lands the readings of its hour into +output/lake/readings//.csv and updates the +region's dataset, which feeds ``usage_rollup``. +""" + +from __future__ import annotations + +import pendulum +from airflow import DAG + +from meter_lib.assets import READINGS, REGIONS +from meter_lib.operators import CsvSliceOperator + +for region in REGIONS: + with DAG( + dag_id=f"readings_{region}_hourly", + schedule_interval="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering", "retries": 2}, + tags=["metering", "ingest"], + ) as dag: + CsvSliceOperator( + task_id="land_readings", + source=f"data/readings_{region}.csv", + ts_column="read_at", + target=f"output/lake/readings/{region}/" + "{{ data_interval_start.strftime('%Y%m%dT%H') }}.csv", + outlets=[READINGS[region]], + ) + globals()[dag.dag_id] = dag diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/publish_invoices.sh b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/publish_invoices.sh new file mode 100644 index 0000000..3567016 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/publish_invoices.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Publishes the billing file of the day billed by this run to the finance dropbox. +set -euo pipefail +day="{{ ti.xcom_pull(task_ids='bill') }}" +dropbox="{{ conf.get('metering', 'invoice_dropbox') }}" +mkdir -p "$dropbox" +cp "$PROJECT_ROOT/output/billing/${day}.csv" "$dropbox/{{ macros.meter.invoice_batch(ti.xcom_pull(task_ids='bill')) }}.csv" +echo "published invoices for ${day}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/sync_tariffs.sh b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/sync_tariffs.sh new file mode 100644 index 0000000..6fef086 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/scripts/sync_tariffs.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Day-ahead prices: the sync for {{ ds }} also refreshes {{ tomorrow_ds }}. +set -euo pipefail +dest="$PROJECT_ROOT/output/lake/tariffs" +mkdir -p "$dest" +for day in {{ ds }} {{ tomorrow_ds }}; do + grep "^${day}," "$PROJECT_ROOT/data/tariffs.csv" > "$dest/${day}.csv" +done +echo "synced tariffs for {{ ds }} and {{ tomorrow_ds }}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/tariffs_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/tariffs_daily.py new file mode 100644 index 0000000..654ecae --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/tariffs_daily.py @@ -0,0 +1,27 @@ +"""Daily tariff sync from the utility's price sheet (data/tariffs.csv).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.bash import BashOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + +with DAG( + dag_id="tariffs_daily", + schedule_interval="0 1 * * *", + start_date=pendulum.datetime(2026, 3, 3, tz="UTC"), + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + default_args={"owner": "metering", "retries": 1}, + tags=["metering", "reference-data"], +) as dag: + BashOperator( + task_id="sync_tariffs", + bash_command="sync_tariffs.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/usage_rollup.py new file mode 100644 index 0000000..c398c6b --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/dags/usage_rollup.py @@ -0,0 +1,67 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.decorators import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(data_interval_start=None, data_interval_end=None, triggering_dataset_events=None) -> str: + start, end = data_interval_start, data_interval_end + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum(event.extra.get("rows", 0) for event in triggering_dataset_events.get(READINGS[region].uri, [])) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_east.csv b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_east.csv new file mode 100644 index 0000000..8594e24 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_east.csv @@ -0,0 +1,865 @@ +meter_id,read_at,kwh +E-101,2026-03-04 00:00:00,0.3 +E-102,2026-03-04 00:03:00,1.4 +E-103,2026-03-04 00:06:00,2.5 +E-101,2026-03-04 00:15:00,1.7 +E-102,2026-03-04 00:18:00,0.5 +E-103,2026-03-04 00:21:00,1.6 +E-101,2026-03-04 00:30:00,0.8 +E-102,2026-03-04 00:33:00,1.9 +E-103,2026-03-04 00:36:00,0.7 +E-101,2026-03-04 00:45:00,2.2 +E-102,2026-03-04 00:48:00,1.0 +E-103,2026-03-04 00:51:00,2.1 +E-101,2026-03-04 01:00:00,1.3 +E-102,2026-03-04 01:03:00,2.4 +E-103,2026-03-04 01:06:00,1.2 +E-101,2026-03-04 01:15:00,0.4 +E-102,2026-03-04 01:18:00,1.5 +E-103,2026-03-04 01:21:00,0.3 +E-101,2026-03-04 01:30:00,1.8 +E-102,2026-03-04 01:33:00,0.6 +E-103,2026-03-04 01:36:00,1.7 +E-101,2026-03-04 01:45:00,0.9 +E-102,2026-03-04 01:48:00,2.0 +E-103,2026-03-04 01:51:00,0.8 +E-101,2026-03-04 02:00:00,2.3 +E-102,2026-03-04 02:03:00,1.1 +E-103,2026-03-04 02:06:00,2.2 +E-101,2026-03-04 02:15:00,1.4 +E-102,2026-03-04 02:18:00,2.5 +E-103,2026-03-04 02:21:00,1.3 +E-101,2026-03-04 02:30:00,0.5 +E-102,2026-03-04 02:33:00,1.6 +E-103,2026-03-04 02:36:00,0.4 +E-101,2026-03-04 02:45:00,1.9 +E-102,2026-03-04 02:48:00,0.7 +E-103,2026-03-04 02:51:00,1.8 +E-101,2026-03-04 03:00:00,1.0 +E-102,2026-03-04 03:03:00,2.1 +E-103,2026-03-04 03:06:00,0.9 +E-101,2026-03-04 03:15:00,2.4 +E-102,2026-03-04 03:18:00,1.2 +E-103,2026-03-04 03:21:00,2.3 +E-101,2026-03-04 03:30:00,1.5 +E-102,2026-03-04 03:33:00,0.3 +E-103,2026-03-04 03:36:00,1.4 +E-101,2026-03-04 03:45:00,0.6 +E-102,2026-03-04 03:48:00,1.7 +E-103,2026-03-04 03:51:00,0.5 +E-101,2026-03-04 04:00:00,2.0 +E-102,2026-03-04 04:03:00,0.8 +E-103,2026-03-04 04:06:00,1.9 +E-101,2026-03-04 04:15:00,1.1 +E-102,2026-03-04 04:18:00,2.2 +E-103,2026-03-04 04:21:00,1.0 +E-101,2026-03-04 04:30:00,2.5 +E-102,2026-03-04 04:33:00,1.3 +E-103,2026-03-04 04:36:00,2.4 +E-101,2026-03-04 04:45:00,1.6 +E-102,2026-03-04 04:48:00,0.4 +E-103,2026-03-04 04:51:00,1.5 +E-101,2026-03-04 05:00:00,0.7 +E-102,2026-03-04 05:03:00,1.8 +E-103,2026-03-04 05:06:00,0.6 +E-101,2026-03-04 05:15:00,2.1 +E-102,2026-03-04 05:18:00,0.9 +E-103,2026-03-04 05:21:00,2.0 +E-101,2026-03-04 05:30:00,1.2 +E-102,2026-03-04 05:33:00,2.3 +E-103,2026-03-04 05:36:00,1.1 +E-101,2026-03-04 05:45:00,0.3 +E-102,2026-03-04 05:48:00,1.4 +E-103,2026-03-04 05:51:00,2.5 +E-101,2026-03-04 06:00:00,1.7 +E-102,2026-03-04 06:03:00,0.5 +E-103,2026-03-04 06:06:00,1.6 +E-101,2026-03-04 06:15:00,0.8 +E-102,2026-03-04 06:18:00,1.9 +E-103,2026-03-04 06:21:00,0.7 +E-101,2026-03-04 06:30:00,2.2 +E-102,2026-03-04 06:33:00,1.0 +E-103,2026-03-04 06:36:00,2.1 +E-101,2026-03-04 06:45:00,1.3 +E-102,2026-03-04 06:48:00,2.4 +E-103,2026-03-04 06:51:00,1.2 +E-101,2026-03-04 07:00:00,0.4 +E-102,2026-03-04 07:03:00,1.5 +E-103,2026-03-04 07:06:00,0.3 +E-101,2026-03-04 07:15:00,1.8 +E-102,2026-03-04 07:18:00,0.6 +E-103,2026-03-04 07:21:00,1.7 +E-101,2026-03-04 07:30:00,0.9 +E-102,2026-03-04 07:33:00,2.0 +E-103,2026-03-04 07:36:00,0.8 +E-101,2026-03-04 07:45:00,2.3 +E-102,2026-03-04 07:48:00,1.1 +E-103,2026-03-04 07:51:00,2.2 +E-101,2026-03-04 08:00:00,1.4 +E-102,2026-03-04 08:03:00,2.5 +E-103,2026-03-04 08:06:00,1.3 +E-101,2026-03-04 08:15:00,0.5 +E-102,2026-03-04 08:18:00,1.6 +E-103,2026-03-04 08:21:00,0.4 +E-101,2026-03-04 08:30:00,1.9 +E-102,2026-03-04 08:33:00,0.7 +E-103,2026-03-04 08:36:00,1.8 +E-101,2026-03-04 08:45:00,1.0 +E-102,2026-03-04 08:48:00,2.1 +E-103,2026-03-04 08:51:00,0.9 +E-101,2026-03-04 09:00:00,2.4 +E-102,2026-03-04 09:03:00,1.2 +E-103,2026-03-04 09:06:00,2.3 +E-101,2026-03-04 09:15:00,1.5 +E-102,2026-03-04 09:18:00,0.3 +E-103,2026-03-04 09:21:00,1.4 +E-101,2026-03-04 09:30:00,0.6 +E-102,2026-03-04 09:33:00,1.7 +E-103,2026-03-04 09:36:00,0.5 +E-101,2026-03-04 09:45:00,2.0 +E-102,2026-03-04 09:48:00,0.8 +E-103,2026-03-04 09:51:00,1.9 +E-101,2026-03-04 10:00:00,1.1 +E-102,2026-03-04 10:03:00,2.2 +E-103,2026-03-04 10:06:00,1.0 +E-101,2026-03-04 10:15:00,2.5 +E-102,2026-03-04 10:18:00,1.3 +E-103,2026-03-04 10:21:00,2.4 +E-101,2026-03-04 10:30:00,1.6 +E-102,2026-03-04 10:33:00,0.4 +E-103,2026-03-04 10:36:00,1.5 +E-101,2026-03-04 10:45:00,0.7 +E-102,2026-03-04 10:48:00,1.8 +E-103,2026-03-04 10:51:00,0.6 +E-101,2026-03-04 11:00:00,2.1 +E-102,2026-03-04 11:03:00,0.9 +E-103,2026-03-04 11:06:00,2.0 +E-101,2026-03-04 11:15:00,1.2 +E-102,2026-03-04 11:18:00,2.3 +E-103,2026-03-04 11:21:00,1.1 +E-101,2026-03-04 11:30:00,0.3 +E-102,2026-03-04 11:33:00,1.4 +E-103,2026-03-04 11:36:00,2.5 +E-101,2026-03-04 11:45:00,1.7 +E-102,2026-03-04 11:48:00,0.5 +E-103,2026-03-04 11:51:00,1.6 +E-101,2026-03-04 12:00:00,0.8 +E-102,2026-03-04 12:03:00,1.9 +E-103,2026-03-04 12:06:00,0.7 +E-101,2026-03-04 12:15:00,2.2 +E-102,2026-03-04 12:18:00,1.0 +E-103,2026-03-04 12:21:00,2.1 +E-101,2026-03-04 12:30:00,1.3 +E-102,2026-03-04 12:33:00,2.4 +E-103,2026-03-04 12:36:00,1.2 +E-101,2026-03-04 12:45:00,0.4 +E-102,2026-03-04 12:48:00,1.5 +E-103,2026-03-04 12:51:00,0.3 +E-101,2026-03-04 13:00:00,1.8 +E-102,2026-03-04 13:03:00,0.6 +E-103,2026-03-04 13:06:00,1.7 +E-101,2026-03-04 13:15:00,0.9 +E-102,2026-03-04 13:18:00,2.0 +E-103,2026-03-04 13:21:00,0.8 +E-101,2026-03-04 13:30:00,2.3 +E-102,2026-03-04 13:33:00,1.1 +E-103,2026-03-04 13:36:00,2.2 +E-101,2026-03-04 13:45:00,1.4 +E-102,2026-03-04 13:48:00,2.5 +E-103,2026-03-04 13:51:00,1.3 +E-101,2026-03-04 14:00:00,0.5 +E-102,2026-03-04 14:03:00,1.6 +E-103,2026-03-04 14:06:00,0.4 +E-101,2026-03-04 14:15:00,1.9 +E-102,2026-03-04 14:18:00,0.7 +E-103,2026-03-04 14:21:00,1.8 +E-101,2026-03-04 14:30:00,1.0 +E-102,2026-03-04 14:33:00,2.1 +E-103,2026-03-04 14:36:00,0.9 +E-101,2026-03-04 14:45:00,2.4 +E-102,2026-03-04 14:48:00,1.2 +E-103,2026-03-04 14:51:00,2.3 +E-101,2026-03-04 15:00:00,1.5 +E-102,2026-03-04 15:03:00,0.3 +E-103,2026-03-04 15:06:00,1.4 +E-101,2026-03-04 15:15:00,0.6 +E-102,2026-03-04 15:18:00,1.7 +E-103,2026-03-04 15:21:00,0.5 +E-101,2026-03-04 15:30:00,2.0 +E-102,2026-03-04 15:33:00,0.8 +E-103,2026-03-04 15:36:00,1.9 +E-101,2026-03-04 15:45:00,1.1 +E-102,2026-03-04 15:48:00,2.2 +E-103,2026-03-04 15:51:00,1.0 +E-101,2026-03-04 16:00:00,2.5 +E-102,2026-03-04 16:03:00,1.3 +E-103,2026-03-04 16:06:00,2.4 +E-101,2026-03-04 16:15:00,1.6 +E-102,2026-03-04 16:18:00,0.4 +E-103,2026-03-04 16:21:00,1.5 +E-101,2026-03-04 16:30:00,0.7 +E-102,2026-03-04 16:33:00,1.8 +E-103,2026-03-04 16:36:00,0.6 +E-101,2026-03-04 16:45:00,2.1 +E-102,2026-03-04 16:48:00,0.9 +E-103,2026-03-04 16:51:00,2.0 +E-101,2026-03-04 17:00:00,1.2 +E-102,2026-03-04 17:03:00,2.3 +E-103,2026-03-04 17:06:00,1.1 +E-101,2026-03-04 17:15:00,0.3 +E-102,2026-03-04 17:18:00,1.4 +E-103,2026-03-04 17:21:00,2.5 +E-101,2026-03-04 17:30:00,1.7 +E-102,2026-03-04 17:33:00,0.5 +E-103,2026-03-04 17:36:00,1.6 +E-101,2026-03-04 17:45:00,0.8 +E-102,2026-03-04 17:48:00,1.9 +E-103,2026-03-04 17:51:00,0.7 +E-101,2026-03-04 18:00:00,2.2 +E-102,2026-03-04 18:03:00,1.0 +E-103,2026-03-04 18:06:00,2.1 +E-101,2026-03-04 18:15:00,1.3 +E-102,2026-03-04 18:18:00,2.4 +E-103,2026-03-04 18:21:00,1.2 +E-101,2026-03-04 18:30:00,0.4 +E-102,2026-03-04 18:33:00,1.5 +E-103,2026-03-04 18:36:00,0.3 +E-101,2026-03-04 18:45:00,1.8 +E-102,2026-03-04 18:48:00,0.6 +E-103,2026-03-04 18:51:00,1.7 +E-101,2026-03-04 19:00:00,0.9 +E-102,2026-03-04 19:03:00,2.0 +E-103,2026-03-04 19:06:00,0.8 +E-101,2026-03-04 19:15:00,2.3 +E-102,2026-03-04 19:18:00,1.1 +E-103,2026-03-04 19:21:00,2.2 +E-101,2026-03-04 19:30:00,1.4 +E-102,2026-03-04 19:33:00,2.5 +E-103,2026-03-04 19:36:00,1.3 +E-101,2026-03-04 19:45:00,0.5 +E-102,2026-03-04 19:48:00,1.6 +E-103,2026-03-04 19:51:00,0.4 +E-101,2026-03-04 20:00:00,1.9 +E-102,2026-03-04 20:03:00,0.7 +E-103,2026-03-04 20:06:00,1.8 +E-101,2026-03-04 20:15:00,1.0 +E-102,2026-03-04 20:18:00,2.1 +E-103,2026-03-04 20:21:00,0.9 +E-101,2026-03-04 20:30:00,2.4 +E-102,2026-03-04 20:33:00,1.2 +E-103,2026-03-04 20:36:00,2.3 +E-101,2026-03-04 20:45:00,1.5 +E-102,2026-03-04 20:48:00,0.3 +E-103,2026-03-04 20:51:00,1.4 +E-101,2026-03-04 21:00:00,0.6 +E-102,2026-03-04 21:03:00,1.7 +E-103,2026-03-04 21:06:00,0.5 +E-101,2026-03-04 21:15:00,2.0 +E-102,2026-03-04 21:18:00,0.8 +E-103,2026-03-04 21:21:00,1.9 +E-101,2026-03-04 21:30:00,1.1 +E-102,2026-03-04 21:33:00,2.2 +E-103,2026-03-04 21:36:00,1.0 +E-101,2026-03-04 21:45:00,2.5 +E-102,2026-03-04 21:48:00,1.3 +E-103,2026-03-04 21:51:00,2.4 +E-101,2026-03-04 22:00:00,1.6 +E-102,2026-03-04 22:03:00,0.4 +E-103,2026-03-04 22:06:00,1.5 +E-101,2026-03-04 22:15:00,0.7 +E-102,2026-03-04 22:18:00,1.8 +E-103,2026-03-04 22:21:00,0.6 +E-101,2026-03-04 22:30:00,2.1 +E-102,2026-03-04 22:33:00,0.9 +E-103,2026-03-04 22:36:00,2.0 +E-101,2026-03-04 22:45:00,1.2 +E-102,2026-03-04 22:48:00,2.3 +E-103,2026-03-04 22:51:00,1.1 +E-101,2026-03-04 23:00:00,0.3 +E-102,2026-03-04 23:03:00,1.4 +E-103,2026-03-04 23:06:00,2.5 +E-101,2026-03-04 23:15:00,1.7 +E-102,2026-03-04 23:18:00,0.5 +E-103,2026-03-04 23:21:00,1.6 +E-101,2026-03-04 23:30:00,0.8 +E-102,2026-03-04 23:33:00,1.9 +E-103,2026-03-04 23:36:00,0.7 +E-101,2026-03-04 23:45:00,2.2 +E-102,2026-03-04 23:48:00,1.0 +E-103,2026-03-04 23:51:00,2.1 +E-101,2026-03-05 00:00:00,1.3 +E-102,2026-03-05 00:03:00,2.4 +E-103,2026-03-05 00:06:00,1.2 +E-101,2026-03-05 00:15:00,0.4 +E-102,2026-03-05 00:18:00,1.5 +E-103,2026-03-05 00:21:00,0.3 +E-101,2026-03-05 00:30:00,1.8 +E-102,2026-03-05 00:33:00,0.6 +E-103,2026-03-05 00:36:00,1.7 +E-101,2026-03-05 00:45:00,0.9 +E-102,2026-03-05 00:48:00,2.0 +E-103,2026-03-05 00:51:00,0.8 +E-101,2026-03-05 01:00:00,2.3 +E-102,2026-03-05 01:03:00,1.1 +E-103,2026-03-05 01:06:00,2.2 +E-101,2026-03-05 01:15:00,1.4 +E-102,2026-03-05 01:18:00,2.5 +E-103,2026-03-05 01:21:00,1.3 +E-101,2026-03-05 01:30:00,0.5 +E-102,2026-03-05 01:33:00,1.6 +E-103,2026-03-05 01:36:00,0.4 +E-101,2026-03-05 01:45:00,1.9 +E-102,2026-03-05 01:48:00,0.7 +E-103,2026-03-05 01:51:00,1.8 +E-101,2026-03-05 02:00:00,1.0 +E-102,2026-03-05 02:03:00,2.1 +E-103,2026-03-05 02:06:00,0.9 +E-101,2026-03-05 02:15:00,2.4 +E-102,2026-03-05 02:18:00,1.2 +E-103,2026-03-05 02:21:00,2.3 +E-101,2026-03-05 02:30:00,1.5 +E-102,2026-03-05 02:33:00,0.3 +E-103,2026-03-05 02:36:00,1.4 +E-101,2026-03-05 02:45:00,0.6 +E-102,2026-03-05 02:48:00,1.7 +E-103,2026-03-05 02:51:00,0.5 +E-101,2026-03-05 03:00:00,2.0 +E-102,2026-03-05 03:03:00,0.8 +E-103,2026-03-05 03:06:00,1.9 +E-101,2026-03-05 03:15:00,1.1 +E-102,2026-03-05 03:18:00,2.2 +E-103,2026-03-05 03:21:00,1.0 +E-101,2026-03-05 03:30:00,2.5 +E-102,2026-03-05 03:33:00,1.3 +E-103,2026-03-05 03:36:00,2.4 +E-101,2026-03-05 03:45:00,1.6 +E-102,2026-03-05 03:48:00,0.4 +E-103,2026-03-05 03:51:00,1.5 +E-101,2026-03-05 04:00:00,0.7 +E-102,2026-03-05 04:03:00,1.8 +E-103,2026-03-05 04:06:00,0.6 +E-101,2026-03-05 04:15:00,2.1 +E-102,2026-03-05 04:18:00,0.9 +E-103,2026-03-05 04:21:00,2.0 +E-101,2026-03-05 04:30:00,1.2 +E-102,2026-03-05 04:33:00,2.3 +E-103,2026-03-05 04:36:00,1.1 +E-101,2026-03-05 04:45:00,0.3 +E-102,2026-03-05 04:48:00,1.4 +E-103,2026-03-05 04:51:00,2.5 +E-101,2026-03-05 05:00:00,1.7 +E-102,2026-03-05 05:03:00,0.5 +E-103,2026-03-05 05:06:00,1.6 +E-101,2026-03-05 05:15:00,0.8 +E-102,2026-03-05 05:18:00,1.9 +E-103,2026-03-05 05:21:00,0.7 +E-101,2026-03-05 05:30:00,2.2 +E-102,2026-03-05 05:33:00,1.0 +E-103,2026-03-05 05:36:00,2.1 +E-101,2026-03-05 05:45:00,1.3 +E-102,2026-03-05 05:48:00,2.4 +E-103,2026-03-05 05:51:00,1.2 +E-101,2026-03-05 06:00:00,0.4 +E-102,2026-03-05 06:03:00,1.5 +E-103,2026-03-05 06:06:00,0.3 +E-101,2026-03-05 06:15:00,1.8 +E-102,2026-03-05 06:18:00,0.6 +E-103,2026-03-05 06:21:00,1.7 +E-101,2026-03-05 06:30:00,0.9 +E-102,2026-03-05 06:33:00,2.0 +E-103,2026-03-05 06:36:00,0.8 +E-101,2026-03-05 06:45:00,2.3 +E-102,2026-03-05 06:48:00,1.1 +E-103,2026-03-05 06:51:00,2.2 +E-101,2026-03-05 07:00:00,1.4 +E-102,2026-03-05 07:03:00,2.5 +E-103,2026-03-05 07:06:00,1.3 +E-101,2026-03-05 07:15:00,0.5 +E-102,2026-03-05 07:18:00,1.6 +E-103,2026-03-05 07:21:00,0.4 +E-101,2026-03-05 07:30:00,1.9 +E-102,2026-03-05 07:33:00,0.7 +E-103,2026-03-05 07:36:00,1.8 +E-101,2026-03-05 07:45:00,1.0 +E-102,2026-03-05 07:48:00,2.1 +E-103,2026-03-05 07:51:00,0.9 +E-101,2026-03-05 08:00:00,2.4 +E-102,2026-03-05 08:03:00,1.2 +E-103,2026-03-05 08:06:00,2.3 +E-101,2026-03-05 08:15:00,1.5 +E-102,2026-03-05 08:18:00,0.3 +E-103,2026-03-05 08:21:00,1.4 +E-101,2026-03-05 08:30:00,0.6 +E-102,2026-03-05 08:33:00,1.7 +E-103,2026-03-05 08:36:00,0.5 +E-101,2026-03-05 08:45:00,2.0 +E-102,2026-03-05 08:48:00,0.8 +E-103,2026-03-05 08:51:00,1.9 +E-101,2026-03-05 09:00:00,1.1 +E-102,2026-03-05 09:03:00,2.2 +E-103,2026-03-05 09:06:00,1.0 +E-101,2026-03-05 09:15:00,2.5 +E-102,2026-03-05 09:18:00,1.3 +E-103,2026-03-05 09:21:00,2.4 +E-101,2026-03-05 09:30:00,1.6 +E-102,2026-03-05 09:33:00,0.4 +E-103,2026-03-05 09:36:00,1.5 +E-101,2026-03-05 09:45:00,0.7 +E-102,2026-03-05 09:48:00,1.8 +E-103,2026-03-05 09:51:00,0.6 +E-101,2026-03-05 10:00:00,2.1 +E-102,2026-03-05 10:03:00,0.9 +E-103,2026-03-05 10:06:00,2.0 +E-101,2026-03-05 10:15:00,1.2 +E-102,2026-03-05 10:18:00,2.3 +E-103,2026-03-05 10:21:00,1.1 +E-101,2026-03-05 10:30:00,0.3 +E-102,2026-03-05 10:33:00,1.4 +E-103,2026-03-05 10:36:00,2.5 +E-101,2026-03-05 10:45:00,1.7 +E-102,2026-03-05 10:48:00,0.5 +E-103,2026-03-05 10:51:00,1.6 +E-101,2026-03-05 11:00:00,0.8 +E-102,2026-03-05 11:03:00,1.9 +E-103,2026-03-05 11:06:00,0.7 +E-101,2026-03-05 11:15:00,2.2 +E-102,2026-03-05 11:18:00,1.0 +E-103,2026-03-05 11:21:00,2.1 +E-101,2026-03-05 11:30:00,1.3 +E-102,2026-03-05 11:33:00,2.4 +E-103,2026-03-05 11:36:00,1.2 +E-101,2026-03-05 11:45:00,0.4 +E-102,2026-03-05 11:48:00,1.5 +E-103,2026-03-05 11:51:00,0.3 +E-101,2026-03-05 12:00:00,1.8 +E-102,2026-03-05 12:03:00,0.6 +E-103,2026-03-05 12:06:00,1.7 +E-101,2026-03-05 12:15:00,0.9 +E-102,2026-03-05 12:18:00,2.0 +E-103,2026-03-05 12:21:00,0.8 +E-101,2026-03-05 12:30:00,2.3 +E-102,2026-03-05 12:33:00,1.1 +E-103,2026-03-05 12:36:00,2.2 +E-101,2026-03-05 12:45:00,1.4 +E-102,2026-03-05 12:48:00,2.5 +E-103,2026-03-05 12:51:00,1.3 +E-101,2026-03-05 13:00:00,0.5 +E-102,2026-03-05 13:03:00,1.6 +E-103,2026-03-05 13:06:00,0.4 +E-101,2026-03-05 13:15:00,1.9 +E-102,2026-03-05 13:18:00,0.7 +E-103,2026-03-05 13:21:00,1.8 +E-101,2026-03-05 13:30:00,1.0 +E-102,2026-03-05 13:33:00,2.1 +E-103,2026-03-05 13:36:00,0.9 +E-101,2026-03-05 13:45:00,2.4 +E-102,2026-03-05 13:48:00,1.2 +E-103,2026-03-05 13:51:00,2.3 +E-101,2026-03-05 14:00:00,1.5 +E-102,2026-03-05 14:03:00,0.3 +E-103,2026-03-05 14:06:00,1.4 +E-101,2026-03-05 14:15:00,0.6 +E-102,2026-03-05 14:18:00,1.7 +E-103,2026-03-05 14:21:00,0.5 +E-101,2026-03-05 14:30:00,2.0 +E-102,2026-03-05 14:33:00,0.8 +E-103,2026-03-05 14:36:00,1.9 +E-101,2026-03-05 14:45:00,1.1 +E-102,2026-03-05 14:48:00,2.2 +E-103,2026-03-05 14:51:00,1.0 +E-101,2026-03-05 15:00:00,2.5 +E-102,2026-03-05 15:03:00,1.3 +E-103,2026-03-05 15:06:00,2.4 +E-101,2026-03-05 15:15:00,1.6 +E-102,2026-03-05 15:18:00,0.4 +E-103,2026-03-05 15:21:00,1.5 +E-101,2026-03-05 15:30:00,0.7 +E-102,2026-03-05 15:33:00,1.8 +E-103,2026-03-05 15:36:00,0.6 +E-101,2026-03-05 15:45:00,2.1 +E-102,2026-03-05 15:48:00,0.9 +E-103,2026-03-05 15:51:00,2.0 +E-101,2026-03-05 16:00:00,1.2 +E-102,2026-03-05 16:03:00,2.3 +E-103,2026-03-05 16:06:00,1.1 +E-101,2026-03-05 16:15:00,0.3 +E-102,2026-03-05 16:18:00,1.4 +E-103,2026-03-05 16:21:00,2.5 +E-101,2026-03-05 16:30:00,1.7 +E-102,2026-03-05 16:33:00,0.5 +E-103,2026-03-05 16:36:00,1.6 +E-101,2026-03-05 16:45:00,0.8 +E-102,2026-03-05 16:48:00,1.9 +E-103,2026-03-05 16:51:00,0.7 +E-101,2026-03-05 17:00:00,2.2 +E-102,2026-03-05 17:03:00,1.0 +E-103,2026-03-05 17:06:00,2.1 +E-101,2026-03-05 17:15:00,1.3 +E-102,2026-03-05 17:18:00,2.4 +E-103,2026-03-05 17:21:00,1.2 +E-101,2026-03-05 17:30:00,0.4 +E-102,2026-03-05 17:33:00,1.5 +E-103,2026-03-05 17:36:00,0.3 +E-101,2026-03-05 17:45:00,1.8 +E-102,2026-03-05 17:48:00,0.6 +E-103,2026-03-05 17:51:00,1.7 +E-101,2026-03-05 18:00:00,0.9 +E-102,2026-03-05 18:03:00,2.0 +E-103,2026-03-05 18:06:00,0.8 +E-101,2026-03-05 18:15:00,2.3 +E-102,2026-03-05 18:18:00,1.1 +E-103,2026-03-05 18:21:00,2.2 +E-101,2026-03-05 18:30:00,1.4 +E-102,2026-03-05 18:33:00,2.5 +E-103,2026-03-05 18:36:00,1.3 +E-101,2026-03-05 18:45:00,0.5 +E-102,2026-03-05 18:48:00,1.6 +E-103,2026-03-05 18:51:00,0.4 +E-101,2026-03-05 19:00:00,1.9 +E-102,2026-03-05 19:03:00,0.7 +E-103,2026-03-05 19:06:00,1.8 +E-101,2026-03-05 19:15:00,1.0 +E-102,2026-03-05 19:18:00,2.1 +E-103,2026-03-05 19:21:00,0.9 +E-101,2026-03-05 19:30:00,2.4 +E-102,2026-03-05 19:33:00,1.2 +E-103,2026-03-05 19:36:00,2.3 +E-101,2026-03-05 19:45:00,1.5 +E-102,2026-03-05 19:48:00,0.3 +E-103,2026-03-05 19:51:00,1.4 +E-101,2026-03-05 20:00:00,0.6 +E-102,2026-03-05 20:03:00,1.7 +E-103,2026-03-05 20:06:00,0.5 +E-101,2026-03-05 20:15:00,2.0 +E-102,2026-03-05 20:18:00,0.8 +E-103,2026-03-05 20:21:00,1.9 +E-101,2026-03-05 20:30:00,1.1 +E-102,2026-03-05 20:33:00,2.2 +E-103,2026-03-05 20:36:00,1.0 +E-101,2026-03-05 20:45:00,2.5 +E-102,2026-03-05 20:48:00,1.3 +E-103,2026-03-05 20:51:00,2.4 +E-101,2026-03-05 21:00:00,1.6 +E-102,2026-03-05 21:03:00,0.4 +E-103,2026-03-05 21:06:00,1.5 +E-101,2026-03-05 21:15:00,0.7 +E-102,2026-03-05 21:18:00,1.8 +E-103,2026-03-05 21:21:00,0.6 +E-101,2026-03-05 21:30:00,2.1 +E-102,2026-03-05 21:33:00,0.9 +E-103,2026-03-05 21:36:00,2.0 +E-101,2026-03-05 21:45:00,1.2 +E-102,2026-03-05 21:48:00,2.3 +E-103,2026-03-05 21:51:00,1.1 +E-101,2026-03-05 22:00:00,0.3 +E-102,2026-03-05 22:03:00,1.4 +E-103,2026-03-05 22:06:00,2.5 +E-101,2026-03-05 22:15:00,1.7 +E-102,2026-03-05 22:18:00,0.5 +E-103,2026-03-05 22:21:00,1.6 +E-101,2026-03-05 22:30:00,0.8 +E-102,2026-03-05 22:33:00,1.9 +E-103,2026-03-05 22:36:00,0.7 +E-101,2026-03-05 22:45:00,2.2 +E-102,2026-03-05 22:48:00,1.0 +E-103,2026-03-05 22:51:00,2.1 +E-101,2026-03-05 23:00:00,1.3 +E-102,2026-03-05 23:03:00,2.4 +E-103,2026-03-05 23:06:00,1.2 +E-101,2026-03-05 23:15:00,0.4 +E-102,2026-03-05 23:18:00,1.5 +E-103,2026-03-05 23:21:00,0.3 +E-101,2026-03-05 23:30:00,1.8 +E-102,2026-03-05 23:33:00,0.6 +E-103,2026-03-05 23:36:00,1.7 +E-101,2026-03-05 23:45:00,0.9 +E-102,2026-03-05 23:48:00,2.0 +E-103,2026-03-05 23:51:00,0.8 +E-101,2026-03-06 00:00:00,2.3 +E-102,2026-03-06 00:03:00,1.1 +E-103,2026-03-06 00:06:00,2.2 +E-101,2026-03-06 00:15:00,1.4 +E-102,2026-03-06 00:18:00,2.5 +E-103,2026-03-06 00:21:00,1.3 +E-101,2026-03-06 00:30:00,0.5 +E-102,2026-03-06 00:33:00,1.6 +E-103,2026-03-06 00:36:00,0.4 +E-101,2026-03-06 00:45:00,1.9 +E-102,2026-03-06 00:48:00,0.7 +E-103,2026-03-06 00:51:00,1.8 +E-101,2026-03-06 01:00:00,1.0 +E-102,2026-03-06 01:03:00,2.1 +E-103,2026-03-06 01:06:00,0.9 +E-101,2026-03-06 01:15:00,2.4 +E-102,2026-03-06 01:18:00,1.2 +E-103,2026-03-06 01:21:00,2.3 +E-101,2026-03-06 01:30:00,1.5 +E-102,2026-03-06 01:33:00,0.3 +E-103,2026-03-06 01:36:00,1.4 +E-101,2026-03-06 01:45:00,0.6 +E-102,2026-03-06 01:48:00,1.7 +E-103,2026-03-06 01:51:00,0.5 +E-101,2026-03-06 02:00:00,2.0 +E-102,2026-03-06 02:03:00,0.8 +E-103,2026-03-06 02:06:00,1.9 +E-101,2026-03-06 02:15:00,1.1 +E-102,2026-03-06 02:18:00,2.2 +E-103,2026-03-06 02:21:00,1.0 +E-101,2026-03-06 02:30:00,2.5 +E-102,2026-03-06 02:33:00,1.3 +E-103,2026-03-06 02:36:00,2.4 +E-101,2026-03-06 02:45:00,1.6 +E-102,2026-03-06 02:48:00,0.4 +E-103,2026-03-06 02:51:00,1.5 +E-101,2026-03-06 03:00:00,0.7 +E-102,2026-03-06 03:03:00,1.8 +E-103,2026-03-06 03:06:00,0.6 +E-101,2026-03-06 03:15:00,2.1 +E-102,2026-03-06 03:18:00,0.9 +E-103,2026-03-06 03:21:00,2.0 +E-101,2026-03-06 03:30:00,1.2 +E-102,2026-03-06 03:33:00,2.3 +E-103,2026-03-06 03:36:00,1.1 +E-101,2026-03-06 03:45:00,0.3 +E-102,2026-03-06 03:48:00,1.4 +E-103,2026-03-06 03:51:00,2.5 +E-101,2026-03-06 04:00:00,1.7 +E-102,2026-03-06 04:03:00,0.5 +E-103,2026-03-06 04:06:00,1.6 +E-101,2026-03-06 04:15:00,0.8 +E-102,2026-03-06 04:18:00,1.9 +E-103,2026-03-06 04:21:00,0.7 +E-101,2026-03-06 04:30:00,2.2 +E-102,2026-03-06 04:33:00,1.0 +E-103,2026-03-06 04:36:00,2.1 +E-101,2026-03-06 04:45:00,1.3 +E-102,2026-03-06 04:48:00,2.4 +E-103,2026-03-06 04:51:00,1.2 +E-101,2026-03-06 05:00:00,0.4 +E-102,2026-03-06 05:03:00,1.5 +E-103,2026-03-06 05:06:00,0.3 +E-101,2026-03-06 05:15:00,1.8 +E-102,2026-03-06 05:18:00,0.6 +E-103,2026-03-06 05:21:00,1.7 +E-101,2026-03-06 05:30:00,0.9 +E-102,2026-03-06 05:33:00,2.0 +E-103,2026-03-06 05:36:00,0.8 +E-101,2026-03-06 05:45:00,2.3 +E-102,2026-03-06 05:48:00,1.1 +E-103,2026-03-06 05:51:00,2.2 +E-101,2026-03-06 06:00:00,1.4 +E-102,2026-03-06 06:03:00,2.5 +E-103,2026-03-06 06:06:00,1.3 +E-101,2026-03-06 06:15:00,0.5 +E-102,2026-03-06 06:18:00,1.6 +E-103,2026-03-06 06:21:00,0.4 +E-101,2026-03-06 06:30:00,1.9 +E-102,2026-03-06 06:33:00,0.7 +E-103,2026-03-06 06:36:00,1.8 +E-101,2026-03-06 06:45:00,1.0 +E-102,2026-03-06 06:48:00,2.1 +E-103,2026-03-06 06:51:00,0.9 +E-101,2026-03-06 07:00:00,2.4 +E-102,2026-03-06 07:03:00,1.2 +E-103,2026-03-06 07:06:00,2.3 +E-101,2026-03-06 07:15:00,1.5 +E-102,2026-03-06 07:18:00,0.3 +E-103,2026-03-06 07:21:00,1.4 +E-101,2026-03-06 07:30:00,0.6 +E-102,2026-03-06 07:33:00,1.7 +E-103,2026-03-06 07:36:00,0.5 +E-101,2026-03-06 07:45:00,2.0 +E-102,2026-03-06 07:48:00,0.8 +E-103,2026-03-06 07:51:00,1.9 +E-101,2026-03-06 08:00:00,1.1 +E-102,2026-03-06 08:03:00,2.2 +E-103,2026-03-06 08:06:00,1.0 +E-101,2026-03-06 08:15:00,2.5 +E-102,2026-03-06 08:18:00,1.3 +E-103,2026-03-06 08:21:00,2.4 +E-101,2026-03-06 08:30:00,1.6 +E-102,2026-03-06 08:33:00,0.4 +E-103,2026-03-06 08:36:00,1.5 +E-101,2026-03-06 08:45:00,0.7 +E-102,2026-03-06 08:48:00,1.8 +E-103,2026-03-06 08:51:00,0.6 +E-101,2026-03-06 09:00:00,2.1 +E-102,2026-03-06 09:03:00,0.9 +E-103,2026-03-06 09:06:00,2.0 +E-101,2026-03-06 09:15:00,1.2 +E-102,2026-03-06 09:18:00,2.3 +E-103,2026-03-06 09:21:00,1.1 +E-101,2026-03-06 09:30:00,0.3 +E-102,2026-03-06 09:33:00,1.4 +E-103,2026-03-06 09:36:00,2.5 +E-101,2026-03-06 09:45:00,1.7 +E-102,2026-03-06 09:48:00,0.5 +E-103,2026-03-06 09:51:00,1.6 +E-101,2026-03-06 10:00:00,0.8 +E-102,2026-03-06 10:03:00,1.9 +E-103,2026-03-06 10:06:00,0.7 +E-101,2026-03-06 10:15:00,2.2 +E-102,2026-03-06 10:18:00,1.0 +E-103,2026-03-06 10:21:00,2.1 +E-101,2026-03-06 10:30:00,1.3 +E-102,2026-03-06 10:33:00,2.4 +E-103,2026-03-06 10:36:00,1.2 +E-101,2026-03-06 10:45:00,0.4 +E-102,2026-03-06 10:48:00,1.5 +E-103,2026-03-06 10:51:00,0.3 +E-101,2026-03-06 11:00:00,1.8 +E-102,2026-03-06 11:03:00,0.6 +E-103,2026-03-06 11:06:00,1.7 +E-101,2026-03-06 11:15:00,0.9 +E-102,2026-03-06 11:18:00,2.0 +E-103,2026-03-06 11:21:00,0.8 +E-101,2026-03-06 11:30:00,2.3 +E-102,2026-03-06 11:33:00,1.1 +E-103,2026-03-06 11:36:00,2.2 +E-101,2026-03-06 11:45:00,1.4 +E-102,2026-03-06 11:48:00,2.5 +E-103,2026-03-06 11:51:00,1.3 +E-101,2026-03-06 12:00:00,0.5 +E-102,2026-03-06 12:03:00,1.6 +E-103,2026-03-06 12:06:00,0.4 +E-101,2026-03-06 12:15:00,1.9 +E-102,2026-03-06 12:18:00,0.7 +E-103,2026-03-06 12:21:00,1.8 +E-101,2026-03-06 12:30:00,1.0 +E-102,2026-03-06 12:33:00,2.1 +E-103,2026-03-06 12:36:00,0.9 +E-101,2026-03-06 12:45:00,2.4 +E-102,2026-03-06 12:48:00,1.2 +E-103,2026-03-06 12:51:00,2.3 +E-101,2026-03-06 13:00:00,1.5 +E-102,2026-03-06 13:03:00,0.3 +E-103,2026-03-06 13:06:00,1.4 +E-101,2026-03-06 13:15:00,0.6 +E-102,2026-03-06 13:18:00,1.7 +E-103,2026-03-06 13:21:00,0.5 +E-101,2026-03-06 13:30:00,2.0 +E-102,2026-03-06 13:33:00,0.8 +E-103,2026-03-06 13:36:00,1.9 +E-101,2026-03-06 13:45:00,1.1 +E-102,2026-03-06 13:48:00,2.2 +E-103,2026-03-06 13:51:00,1.0 +E-101,2026-03-06 14:00:00,2.5 +E-102,2026-03-06 14:03:00,1.3 +E-103,2026-03-06 14:06:00,2.4 +E-101,2026-03-06 14:15:00,1.6 +E-102,2026-03-06 14:18:00,0.4 +E-103,2026-03-06 14:21:00,1.5 +E-101,2026-03-06 14:30:00,0.7 +E-102,2026-03-06 14:33:00,1.8 +E-103,2026-03-06 14:36:00,0.6 +E-101,2026-03-06 14:45:00,2.1 +E-102,2026-03-06 14:48:00,0.9 +E-103,2026-03-06 14:51:00,2.0 +E-101,2026-03-06 15:00:00,1.2 +E-102,2026-03-06 15:03:00,2.3 +E-103,2026-03-06 15:06:00,1.1 +E-101,2026-03-06 15:15:00,0.3 +E-102,2026-03-06 15:18:00,1.4 +E-103,2026-03-06 15:21:00,2.5 +E-101,2026-03-06 15:30:00,1.7 +E-102,2026-03-06 15:33:00,0.5 +E-103,2026-03-06 15:36:00,1.6 +E-101,2026-03-06 15:45:00,0.8 +E-102,2026-03-06 15:48:00,1.9 +E-103,2026-03-06 15:51:00,0.7 +E-101,2026-03-06 16:00:00,2.2 +E-102,2026-03-06 16:03:00,1.0 +E-103,2026-03-06 16:06:00,2.1 +E-101,2026-03-06 16:15:00,1.3 +E-102,2026-03-06 16:18:00,2.4 +E-103,2026-03-06 16:21:00,1.2 +E-101,2026-03-06 16:30:00,0.4 +E-102,2026-03-06 16:33:00,1.5 +E-103,2026-03-06 16:36:00,0.3 +E-101,2026-03-06 16:45:00,1.8 +E-102,2026-03-06 16:48:00,0.6 +E-103,2026-03-06 16:51:00,1.7 +E-101,2026-03-06 17:00:00,0.9 +E-102,2026-03-06 17:03:00,2.0 +E-103,2026-03-06 17:06:00,0.8 +E-101,2026-03-06 17:15:00,2.3 +E-102,2026-03-06 17:18:00,1.1 +E-103,2026-03-06 17:21:00,2.2 +E-101,2026-03-06 17:30:00,1.4 +E-102,2026-03-06 17:33:00,2.5 +E-103,2026-03-06 17:36:00,1.3 +E-101,2026-03-06 17:45:00,0.5 +E-102,2026-03-06 17:48:00,1.6 +E-103,2026-03-06 17:51:00,0.4 +E-101,2026-03-06 18:00:00,1.9 +E-102,2026-03-06 18:03:00,0.7 +E-103,2026-03-06 18:06:00,1.8 +E-101,2026-03-06 18:15:00,1.0 +E-102,2026-03-06 18:18:00,2.1 +E-103,2026-03-06 18:21:00,0.9 +E-101,2026-03-06 18:30:00,2.4 +E-102,2026-03-06 18:33:00,1.2 +E-103,2026-03-06 18:36:00,2.3 +E-101,2026-03-06 18:45:00,1.5 +E-102,2026-03-06 18:48:00,0.3 +E-103,2026-03-06 18:51:00,1.4 +E-101,2026-03-06 19:00:00,0.6 +E-102,2026-03-06 19:03:00,1.7 +E-103,2026-03-06 19:06:00,0.5 +E-101,2026-03-06 19:15:00,2.0 +E-102,2026-03-06 19:18:00,0.8 +E-103,2026-03-06 19:21:00,1.9 +E-101,2026-03-06 19:30:00,1.1 +E-102,2026-03-06 19:33:00,2.2 +E-103,2026-03-06 19:36:00,1.0 +E-101,2026-03-06 19:45:00,2.5 +E-102,2026-03-06 19:48:00,1.3 +E-103,2026-03-06 19:51:00,2.4 +E-101,2026-03-06 20:00:00,1.6 +E-102,2026-03-06 20:03:00,0.4 +E-103,2026-03-06 20:06:00,1.5 +E-101,2026-03-06 20:15:00,0.7 +E-102,2026-03-06 20:18:00,1.8 +E-103,2026-03-06 20:21:00,0.6 +E-101,2026-03-06 20:30:00,2.1 +E-102,2026-03-06 20:33:00,0.9 +E-103,2026-03-06 20:36:00,2.0 +E-101,2026-03-06 20:45:00,1.2 +E-102,2026-03-06 20:48:00,2.3 +E-103,2026-03-06 20:51:00,1.1 +E-101,2026-03-06 21:00:00,0.3 +E-102,2026-03-06 21:03:00,1.4 +E-103,2026-03-06 21:06:00,2.5 +E-101,2026-03-06 21:15:00,1.7 +E-102,2026-03-06 21:18:00,0.5 +E-103,2026-03-06 21:21:00,1.6 +E-101,2026-03-06 21:30:00,0.8 +E-102,2026-03-06 21:33:00,1.9 +E-103,2026-03-06 21:36:00,0.7 +E-101,2026-03-06 21:45:00,2.2 +E-102,2026-03-06 21:48:00,1.0 +E-103,2026-03-06 21:51:00,2.1 +E-101,2026-03-06 22:00:00,1.3 +E-102,2026-03-06 22:03:00,2.4 +E-103,2026-03-06 22:06:00,1.2 +E-101,2026-03-06 22:15:00,0.4 +E-102,2026-03-06 22:18:00,1.5 +E-103,2026-03-06 22:21:00,0.3 +E-101,2026-03-06 22:30:00,1.8 +E-102,2026-03-06 22:33:00,0.6 +E-103,2026-03-06 22:36:00,1.7 +E-101,2026-03-06 22:45:00,0.9 +E-102,2026-03-06 22:48:00,2.0 +E-103,2026-03-06 22:51:00,0.8 +E-101,2026-03-06 23:00:00,2.3 +E-102,2026-03-06 23:03:00,1.1 +E-103,2026-03-06 23:06:00,2.2 +E-101,2026-03-06 23:15:00,1.4 +E-102,2026-03-06 23:18:00,2.5 +E-103,2026-03-06 23:21:00,1.3 +E-101,2026-03-06 23:30:00,0.5 +E-102,2026-03-06 23:33:00,1.6 +E-103,2026-03-06 23:36:00,0.4 +E-101,2026-03-06 23:45:00,1.9 +E-102,2026-03-06 23:48:00,0.7 +E-103,2026-03-06 23:51:00,1.8 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_west.csv b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_west.csv new file mode 100644 index 0000000..92a585e --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/readings_west.csv @@ -0,0 +1,577 @@ +meter_id,read_at,kwh +W-201,2026-03-04 00:00:00,0.8 +W-202,2026-03-04 00:03:00,1.9 +W-201,2026-03-04 00:15:00,2.2 +W-202,2026-03-04 00:18:00,1.0 +W-201,2026-03-04 00:30:00,1.3 +W-202,2026-03-04 00:33:00,2.4 +W-201,2026-03-04 00:45:00,0.4 +W-202,2026-03-04 00:48:00,1.5 +W-201,2026-03-04 01:00:00,1.8 +W-202,2026-03-04 01:03:00,0.6 +W-201,2026-03-04 01:15:00,0.9 +W-202,2026-03-04 01:18:00,2.0 +W-201,2026-03-04 01:30:00,2.3 +W-202,2026-03-04 01:33:00,1.1 +W-201,2026-03-04 01:45:00,1.4 +W-202,2026-03-04 01:48:00,2.5 +W-201,2026-03-04 02:00:00,0.5 +W-202,2026-03-04 02:03:00,1.6 +W-201,2026-03-04 02:15:00,1.9 +W-202,2026-03-04 02:18:00,0.7 +W-201,2026-03-04 02:30:00,1.0 +W-202,2026-03-04 02:33:00,2.1 +W-201,2026-03-04 02:45:00,2.4 +W-202,2026-03-04 02:48:00,1.2 +W-201,2026-03-04 03:00:00,1.5 +W-202,2026-03-04 03:03:00,0.3 +W-201,2026-03-04 03:15:00,0.6 +W-202,2026-03-04 03:18:00,1.7 +W-201,2026-03-04 03:30:00,2.0 +W-202,2026-03-04 03:33:00,0.8 +W-201,2026-03-04 03:45:00,1.1 +W-202,2026-03-04 03:48:00,2.2 +W-201,2026-03-04 04:00:00,2.5 +W-202,2026-03-04 04:03:00,1.3 +W-201,2026-03-04 04:15:00,1.6 +W-202,2026-03-04 04:18:00,0.4 +W-201,2026-03-04 04:30:00,0.7 +W-202,2026-03-04 04:33:00,1.8 +W-201,2026-03-04 04:45:00,2.1 +W-202,2026-03-04 04:48:00,0.9 +W-201,2026-03-04 05:00:00,1.2 +W-202,2026-03-04 05:03:00,2.3 +W-201,2026-03-04 05:15:00,0.3 +W-202,2026-03-04 05:18:00,1.4 +W-201,2026-03-04 05:30:00,1.7 +W-202,2026-03-04 05:33:00,0.5 +W-201,2026-03-04 05:45:00,0.8 +W-202,2026-03-04 05:48:00,1.9 +W-201,2026-03-04 06:00:00,2.2 +W-202,2026-03-04 06:03:00,1.0 +W-201,2026-03-04 06:15:00,1.3 +W-202,2026-03-04 06:18:00,2.4 +W-201,2026-03-04 06:30:00,0.4 +W-202,2026-03-04 06:33:00,1.5 +W-201,2026-03-04 06:45:00,1.8 +W-202,2026-03-04 06:48:00,0.6 +W-201,2026-03-04 07:00:00,0.9 +W-202,2026-03-04 07:03:00,2.0 +W-201,2026-03-04 07:15:00,2.3 +W-202,2026-03-04 07:18:00,1.1 +W-201,2026-03-04 07:30:00,1.4 +W-202,2026-03-04 07:33:00,2.5 +W-201,2026-03-04 07:45:00,0.5 +W-202,2026-03-04 07:48:00,1.6 +W-201,2026-03-04 08:00:00,1.9 +W-202,2026-03-04 08:03:00,0.7 +W-201,2026-03-04 08:15:00,1.0 +W-202,2026-03-04 08:18:00,2.1 +W-201,2026-03-04 08:30:00,2.4 +W-202,2026-03-04 08:33:00,1.2 +W-201,2026-03-04 08:45:00,1.5 +W-202,2026-03-04 08:48:00,0.3 +W-201,2026-03-04 09:00:00,0.6 +W-202,2026-03-04 09:03:00,1.7 +W-201,2026-03-04 09:15:00,2.0 +W-202,2026-03-04 09:18:00,0.8 +W-201,2026-03-04 09:30:00,1.1 +W-202,2026-03-04 09:33:00,2.2 +W-201,2026-03-04 09:45:00,2.5 +W-202,2026-03-04 09:48:00,1.3 +W-201,2026-03-04 10:00:00,1.6 +W-202,2026-03-04 10:03:00,0.4 +W-201,2026-03-04 10:15:00,0.7 +W-202,2026-03-04 10:18:00,1.8 +W-201,2026-03-04 10:30:00,2.1 +W-202,2026-03-04 10:33:00,0.9 +W-201,2026-03-04 10:45:00,1.2 +W-202,2026-03-04 10:48:00,2.3 +W-201,2026-03-04 11:00:00,0.3 +W-202,2026-03-04 11:03:00,1.4 +W-201,2026-03-04 11:15:00,1.7 +W-202,2026-03-04 11:18:00,0.5 +W-201,2026-03-04 11:30:00,0.8 +W-202,2026-03-04 11:33:00,1.9 +W-201,2026-03-04 11:45:00,2.2 +W-202,2026-03-04 11:48:00,1.0 +W-201,2026-03-04 12:00:00,1.3 +W-202,2026-03-04 12:03:00,2.4 +W-201,2026-03-04 12:15:00,0.4 +W-202,2026-03-04 12:18:00,1.5 +W-201,2026-03-04 12:30:00,1.8 +W-202,2026-03-04 12:33:00,0.6 +W-201,2026-03-04 12:45:00,0.9 +W-202,2026-03-04 12:48:00,2.0 +W-201,2026-03-04 13:00:00,2.3 +W-202,2026-03-04 13:03:00,1.1 +W-201,2026-03-04 13:15:00,1.4 +W-202,2026-03-04 13:18:00,2.5 +W-201,2026-03-04 13:30:00,0.5 +W-202,2026-03-04 13:33:00,1.6 +W-201,2026-03-04 13:45:00,1.9 +W-202,2026-03-04 13:48:00,0.7 +W-201,2026-03-04 14:00:00,1.0 +W-202,2026-03-04 14:03:00,2.1 +W-201,2026-03-04 14:15:00,2.4 +W-202,2026-03-04 14:18:00,1.2 +W-201,2026-03-04 14:30:00,1.5 +W-202,2026-03-04 14:33:00,0.3 +W-201,2026-03-04 14:45:00,0.6 +W-202,2026-03-04 14:48:00,1.7 +W-201,2026-03-04 15:00:00,2.0 +W-202,2026-03-04 15:03:00,0.8 +W-201,2026-03-04 15:15:00,1.1 +W-202,2026-03-04 15:18:00,2.2 +W-201,2026-03-04 15:30:00,2.5 +W-202,2026-03-04 15:33:00,1.3 +W-201,2026-03-04 15:45:00,1.6 +W-202,2026-03-04 15:48:00,0.4 +W-201,2026-03-04 16:00:00,0.7 +W-202,2026-03-04 16:03:00,1.8 +W-201,2026-03-04 16:15:00,2.1 +W-202,2026-03-04 16:18:00,0.9 +W-201,2026-03-04 16:30:00,1.2 +W-202,2026-03-04 16:33:00,2.3 +W-201,2026-03-04 16:45:00,0.3 +W-202,2026-03-04 16:48:00,1.4 +W-201,2026-03-04 17:00:00,1.7 +W-202,2026-03-04 17:03:00,0.5 +W-201,2026-03-04 17:15:00,0.8 +W-202,2026-03-04 17:18:00,1.9 +W-201,2026-03-04 17:30:00,2.2 +W-202,2026-03-04 17:33:00,1.0 +W-201,2026-03-04 17:45:00,1.3 +W-202,2026-03-04 17:48:00,2.4 +W-201,2026-03-04 18:00:00,0.4 +W-202,2026-03-04 18:03:00,1.5 +W-201,2026-03-04 18:15:00,1.8 +W-202,2026-03-04 18:18:00,0.6 +W-201,2026-03-04 18:30:00,0.9 +W-202,2026-03-04 18:33:00,2.0 +W-201,2026-03-04 18:45:00,2.3 +W-202,2026-03-04 18:48:00,1.1 +W-201,2026-03-04 19:00:00,1.4 +W-202,2026-03-04 19:03:00,2.5 +W-201,2026-03-04 19:15:00,0.5 +W-202,2026-03-04 19:18:00,1.6 +W-201,2026-03-04 19:30:00,1.9 +W-202,2026-03-04 19:33:00,0.7 +W-201,2026-03-04 19:45:00,1.0 +W-202,2026-03-04 19:48:00,2.1 +W-201,2026-03-04 20:00:00,2.4 +W-202,2026-03-04 20:03:00,1.2 +W-201,2026-03-04 20:15:00,1.5 +W-202,2026-03-04 20:18:00,0.3 +W-201,2026-03-04 20:30:00,0.6 +W-202,2026-03-04 20:33:00,1.7 +W-201,2026-03-04 20:45:00,2.0 +W-202,2026-03-04 20:48:00,0.8 +W-201,2026-03-04 21:00:00,1.1 +W-202,2026-03-04 21:03:00,2.2 +W-201,2026-03-04 21:15:00,2.5 +W-202,2026-03-04 21:18:00,1.3 +W-201,2026-03-04 21:30:00,1.6 +W-202,2026-03-04 21:33:00,0.4 +W-201,2026-03-04 21:45:00,0.7 +W-202,2026-03-04 21:48:00,1.8 +W-201,2026-03-04 22:00:00,2.1 +W-202,2026-03-04 22:03:00,0.9 +W-201,2026-03-04 22:15:00,1.2 +W-202,2026-03-04 22:18:00,2.3 +W-201,2026-03-04 22:30:00,0.3 +W-202,2026-03-04 22:33:00,1.4 +W-201,2026-03-04 22:45:00,1.7 +W-202,2026-03-04 22:48:00,0.5 +W-201,2026-03-04 23:00:00,0.8 +W-202,2026-03-04 23:03:00,1.9 +W-201,2026-03-04 23:15:00,2.2 +W-202,2026-03-04 23:18:00,1.0 +W-201,2026-03-04 23:30:00,1.3 +W-202,2026-03-04 23:33:00,2.4 +W-201,2026-03-04 23:45:00,0.4 +W-202,2026-03-04 23:48:00,1.5 +W-201,2026-03-05 00:00:00,1.8 +W-202,2026-03-05 00:03:00,0.6 +W-201,2026-03-05 00:15:00,0.9 +W-202,2026-03-05 00:18:00,2.0 +W-201,2026-03-05 00:30:00,2.3 +W-202,2026-03-05 00:33:00,1.1 +W-201,2026-03-05 00:45:00,1.4 +W-202,2026-03-05 00:48:00,2.5 +W-201,2026-03-05 01:00:00,0.5 +W-202,2026-03-05 01:03:00,1.6 +W-201,2026-03-05 01:15:00,1.9 +W-202,2026-03-05 01:18:00,0.7 +W-201,2026-03-05 01:30:00,1.0 +W-202,2026-03-05 01:33:00,2.1 +W-201,2026-03-05 01:45:00,2.4 +W-202,2026-03-05 01:48:00,1.2 +W-201,2026-03-05 02:00:00,1.5 +W-202,2026-03-05 02:03:00,0.3 +W-201,2026-03-05 02:15:00,0.6 +W-202,2026-03-05 02:18:00,1.7 +W-201,2026-03-05 02:30:00,2.0 +W-202,2026-03-05 02:33:00,0.8 +W-201,2026-03-05 02:45:00,1.1 +W-202,2026-03-05 02:48:00,2.2 +W-201,2026-03-05 03:00:00,2.5 +W-202,2026-03-05 03:03:00,1.3 +W-201,2026-03-05 03:15:00,1.6 +W-202,2026-03-05 03:18:00,0.4 +W-201,2026-03-05 03:30:00,0.7 +W-202,2026-03-05 03:33:00,1.8 +W-201,2026-03-05 03:45:00,2.1 +W-202,2026-03-05 03:48:00,0.9 +W-201,2026-03-05 04:00:00,1.2 +W-202,2026-03-05 04:03:00,2.3 +W-201,2026-03-05 04:15:00,0.3 +W-202,2026-03-05 04:18:00,1.4 +W-201,2026-03-05 04:30:00,1.7 +W-202,2026-03-05 04:33:00,0.5 +W-201,2026-03-05 04:45:00,0.8 +W-202,2026-03-05 04:48:00,1.9 +W-201,2026-03-05 05:00:00,2.2 +W-202,2026-03-05 05:03:00,1.0 +W-201,2026-03-05 05:15:00,1.3 +W-202,2026-03-05 05:18:00,2.4 +W-201,2026-03-05 05:30:00,0.4 +W-202,2026-03-05 05:33:00,1.5 +W-201,2026-03-05 05:45:00,1.8 +W-202,2026-03-05 05:48:00,0.6 +W-201,2026-03-05 06:00:00,0.9 +W-202,2026-03-05 06:03:00,2.0 +W-201,2026-03-05 06:15:00,2.3 +W-202,2026-03-05 06:18:00,1.1 +W-201,2026-03-05 06:30:00,1.4 +W-202,2026-03-05 06:33:00,2.5 +W-201,2026-03-05 06:45:00,0.5 +W-202,2026-03-05 06:48:00,1.6 +W-201,2026-03-05 07:00:00,1.9 +W-202,2026-03-05 07:03:00,0.7 +W-201,2026-03-05 07:15:00,1.0 +W-202,2026-03-05 07:18:00,2.1 +W-201,2026-03-05 07:30:00,2.4 +W-202,2026-03-05 07:33:00,1.2 +W-201,2026-03-05 07:45:00,1.5 +W-202,2026-03-05 07:48:00,0.3 +W-201,2026-03-05 08:00:00,0.6 +W-202,2026-03-05 08:03:00,1.7 +W-201,2026-03-05 08:15:00,2.0 +W-202,2026-03-05 08:18:00,0.8 +W-201,2026-03-05 08:30:00,1.1 +W-202,2026-03-05 08:33:00,2.2 +W-201,2026-03-05 08:45:00,2.5 +W-202,2026-03-05 08:48:00,1.3 +W-201,2026-03-05 09:00:00,1.6 +W-202,2026-03-05 09:03:00,0.4 +W-201,2026-03-05 09:15:00,0.7 +W-202,2026-03-05 09:18:00,1.8 +W-201,2026-03-05 09:30:00,2.1 +W-202,2026-03-05 09:33:00,0.9 +W-201,2026-03-05 09:45:00,1.2 +W-202,2026-03-05 09:48:00,2.3 +W-201,2026-03-05 10:00:00,0.3 +W-202,2026-03-05 10:03:00,1.4 +W-201,2026-03-05 10:15:00,1.7 +W-202,2026-03-05 10:18:00,0.5 +W-201,2026-03-05 10:30:00,0.8 +W-202,2026-03-05 10:33:00,1.9 +W-201,2026-03-05 10:45:00,2.2 +W-202,2026-03-05 10:48:00,1.0 +W-201,2026-03-05 11:00:00,1.3 +W-202,2026-03-05 11:03:00,2.4 +W-201,2026-03-05 11:15:00,0.4 +W-202,2026-03-05 11:18:00,1.5 +W-201,2026-03-05 11:30:00,1.8 +W-202,2026-03-05 11:33:00,0.6 +W-201,2026-03-05 11:45:00,0.9 +W-202,2026-03-05 11:48:00,2.0 +W-201,2026-03-05 12:00:00,2.3 +W-202,2026-03-05 12:03:00,1.1 +W-201,2026-03-05 12:15:00,1.4 +W-202,2026-03-05 12:18:00,2.5 +W-201,2026-03-05 12:30:00,0.5 +W-202,2026-03-05 12:33:00,1.6 +W-201,2026-03-05 12:45:00,1.9 +W-202,2026-03-05 12:48:00,0.7 +W-201,2026-03-05 13:00:00,1.0 +W-202,2026-03-05 13:03:00,2.1 +W-201,2026-03-05 13:15:00,2.4 +W-202,2026-03-05 13:18:00,1.2 +W-201,2026-03-05 13:30:00,1.5 +W-202,2026-03-05 13:33:00,0.3 +W-201,2026-03-05 13:45:00,0.6 +W-202,2026-03-05 13:48:00,1.7 +W-201,2026-03-05 14:00:00,2.0 +W-202,2026-03-05 14:03:00,0.8 +W-201,2026-03-05 14:15:00,1.1 +W-202,2026-03-05 14:18:00,2.2 +W-201,2026-03-05 14:30:00,2.5 +W-202,2026-03-05 14:33:00,1.3 +W-201,2026-03-05 14:45:00,1.6 +W-202,2026-03-05 14:48:00,0.4 +W-201,2026-03-05 15:00:00,0.7 +W-202,2026-03-05 15:03:00,1.8 +W-201,2026-03-05 15:15:00,2.1 +W-202,2026-03-05 15:18:00,0.9 +W-201,2026-03-05 15:30:00,1.2 +W-202,2026-03-05 15:33:00,2.3 +W-201,2026-03-05 15:45:00,0.3 +W-202,2026-03-05 15:48:00,1.4 +W-201,2026-03-05 16:00:00,1.7 +W-202,2026-03-05 16:03:00,0.5 +W-201,2026-03-05 16:15:00,0.8 +W-202,2026-03-05 16:18:00,1.9 +W-201,2026-03-05 16:30:00,2.2 +W-202,2026-03-05 16:33:00,1.0 +W-201,2026-03-05 16:45:00,1.3 +W-202,2026-03-05 16:48:00,2.4 +W-201,2026-03-05 17:00:00,0.4 +W-202,2026-03-05 17:03:00,1.5 +W-201,2026-03-05 17:15:00,1.8 +W-202,2026-03-05 17:18:00,0.6 +W-201,2026-03-05 17:30:00,0.9 +W-202,2026-03-05 17:33:00,2.0 +W-201,2026-03-05 17:45:00,2.3 +W-202,2026-03-05 17:48:00,1.1 +W-201,2026-03-05 18:00:00,1.4 +W-202,2026-03-05 18:03:00,2.5 +W-201,2026-03-05 18:15:00,0.5 +W-202,2026-03-05 18:18:00,1.6 +W-201,2026-03-05 18:30:00,1.9 +W-202,2026-03-05 18:33:00,0.7 +W-201,2026-03-05 18:45:00,1.0 +W-202,2026-03-05 18:48:00,2.1 +W-201,2026-03-05 19:00:00,2.4 +W-202,2026-03-05 19:03:00,1.2 +W-201,2026-03-05 19:15:00,1.5 +W-202,2026-03-05 19:18:00,0.3 +W-201,2026-03-05 19:30:00,0.6 +W-202,2026-03-05 19:33:00,1.7 +W-201,2026-03-05 19:45:00,2.0 +W-202,2026-03-05 19:48:00,0.8 +W-201,2026-03-05 20:00:00,1.1 +W-202,2026-03-05 20:03:00,2.2 +W-201,2026-03-05 20:15:00,2.5 +W-202,2026-03-05 20:18:00,1.3 +W-201,2026-03-05 20:30:00,1.6 +W-202,2026-03-05 20:33:00,0.4 +W-201,2026-03-05 20:45:00,0.7 +W-202,2026-03-05 20:48:00,1.8 +W-201,2026-03-05 21:00:00,2.1 +W-202,2026-03-05 21:03:00,0.9 +W-201,2026-03-05 21:15:00,1.2 +W-202,2026-03-05 21:18:00,2.3 +W-201,2026-03-05 21:30:00,0.3 +W-202,2026-03-05 21:33:00,1.4 +W-201,2026-03-05 21:45:00,1.7 +W-202,2026-03-05 21:48:00,0.5 +W-201,2026-03-05 22:00:00,0.8 +W-202,2026-03-05 22:03:00,1.9 +W-201,2026-03-05 22:15:00,2.2 +W-202,2026-03-05 22:18:00,1.0 +W-201,2026-03-05 22:30:00,1.3 +W-202,2026-03-05 22:33:00,2.4 +W-201,2026-03-05 22:45:00,0.4 +W-202,2026-03-05 22:48:00,1.5 +W-201,2026-03-05 23:00:00,1.8 +W-202,2026-03-05 23:03:00,0.6 +W-201,2026-03-05 23:15:00,0.9 +W-202,2026-03-05 23:18:00,2.0 +W-201,2026-03-05 23:30:00,2.3 +W-202,2026-03-05 23:33:00,1.1 +W-201,2026-03-05 23:45:00,1.4 +W-202,2026-03-05 23:48:00,2.5 +W-201,2026-03-06 00:00:00,0.5 +W-202,2026-03-06 00:03:00,1.6 +W-201,2026-03-06 00:15:00,1.9 +W-202,2026-03-06 00:18:00,0.7 +W-201,2026-03-06 00:30:00,1.0 +W-202,2026-03-06 00:33:00,2.1 +W-201,2026-03-06 00:45:00,2.4 +W-202,2026-03-06 00:48:00,1.2 +W-201,2026-03-06 01:00:00,1.5 +W-202,2026-03-06 01:03:00,0.3 +W-201,2026-03-06 01:15:00,0.6 +W-202,2026-03-06 01:18:00,1.7 +W-201,2026-03-06 01:30:00,2.0 +W-202,2026-03-06 01:33:00,0.8 +W-201,2026-03-06 01:45:00,1.1 +W-202,2026-03-06 01:48:00,2.2 +W-201,2026-03-06 02:00:00,2.5 +W-202,2026-03-06 02:03:00,1.3 +W-201,2026-03-06 02:15:00,1.6 +W-202,2026-03-06 02:18:00,0.4 +W-201,2026-03-06 02:30:00,0.7 +W-202,2026-03-06 02:33:00,1.8 +W-201,2026-03-06 02:45:00,2.1 +W-202,2026-03-06 02:48:00,0.9 +W-201,2026-03-06 03:00:00,1.2 +W-202,2026-03-06 03:03:00,2.3 +W-201,2026-03-06 03:15:00,0.3 +W-202,2026-03-06 03:18:00,1.4 +W-201,2026-03-06 03:30:00,1.7 +W-202,2026-03-06 03:33:00,0.5 +W-201,2026-03-06 03:45:00,0.8 +W-202,2026-03-06 03:48:00,1.9 +W-201,2026-03-06 04:00:00,2.2 +W-202,2026-03-06 04:03:00,1.0 +W-201,2026-03-06 04:15:00,1.3 +W-202,2026-03-06 04:18:00,2.4 +W-201,2026-03-06 04:30:00,0.4 +W-202,2026-03-06 04:33:00,1.5 +W-201,2026-03-06 04:45:00,1.8 +W-202,2026-03-06 04:48:00,0.6 +W-201,2026-03-06 05:00:00,0.9 +W-202,2026-03-06 05:03:00,2.0 +W-201,2026-03-06 05:15:00,2.3 +W-202,2026-03-06 05:18:00,1.1 +W-201,2026-03-06 05:30:00,1.4 +W-202,2026-03-06 05:33:00,2.5 +W-201,2026-03-06 05:45:00,0.5 +W-202,2026-03-06 05:48:00,1.6 +W-201,2026-03-06 06:00:00,1.9 +W-202,2026-03-06 06:03:00,0.7 +W-201,2026-03-06 06:15:00,1.0 +W-202,2026-03-06 06:18:00,2.1 +W-201,2026-03-06 06:30:00,2.4 +W-202,2026-03-06 06:33:00,1.2 +W-201,2026-03-06 06:45:00,1.5 +W-202,2026-03-06 06:48:00,0.3 +W-201,2026-03-06 07:00:00,0.6 +W-202,2026-03-06 07:03:00,1.7 +W-201,2026-03-06 07:15:00,2.0 +W-202,2026-03-06 07:18:00,0.8 +W-201,2026-03-06 07:30:00,1.1 +W-202,2026-03-06 07:33:00,2.2 +W-201,2026-03-06 07:45:00,2.5 +W-202,2026-03-06 07:48:00,1.3 +W-201,2026-03-06 08:00:00,1.6 +W-202,2026-03-06 08:03:00,0.4 +W-201,2026-03-06 08:15:00,0.7 +W-202,2026-03-06 08:18:00,1.8 +W-201,2026-03-06 08:30:00,2.1 +W-202,2026-03-06 08:33:00,0.9 +W-201,2026-03-06 08:45:00,1.2 +W-202,2026-03-06 08:48:00,2.3 +W-201,2026-03-06 09:00:00,0.3 +W-202,2026-03-06 09:03:00,1.4 +W-201,2026-03-06 09:15:00,1.7 +W-202,2026-03-06 09:18:00,0.5 +W-201,2026-03-06 09:30:00,0.8 +W-202,2026-03-06 09:33:00,1.9 +W-201,2026-03-06 09:45:00,2.2 +W-202,2026-03-06 09:48:00,1.0 +W-201,2026-03-06 10:00:00,1.3 +W-202,2026-03-06 10:03:00,2.4 +W-201,2026-03-06 10:15:00,0.4 +W-202,2026-03-06 10:18:00,1.5 +W-201,2026-03-06 10:30:00,1.8 +W-202,2026-03-06 10:33:00,0.6 +W-201,2026-03-06 10:45:00,0.9 +W-202,2026-03-06 10:48:00,2.0 +W-201,2026-03-06 11:00:00,2.3 +W-202,2026-03-06 11:03:00,1.1 +W-201,2026-03-06 11:15:00,1.4 +W-202,2026-03-06 11:18:00,2.5 +W-201,2026-03-06 11:30:00,0.5 +W-202,2026-03-06 11:33:00,1.6 +W-201,2026-03-06 11:45:00,1.9 +W-202,2026-03-06 11:48:00,0.7 +W-201,2026-03-06 12:00:00,1.0 +W-202,2026-03-06 12:03:00,2.1 +W-201,2026-03-06 12:15:00,2.4 +W-202,2026-03-06 12:18:00,1.2 +W-201,2026-03-06 12:30:00,1.5 +W-202,2026-03-06 12:33:00,0.3 +W-201,2026-03-06 12:45:00,0.6 +W-202,2026-03-06 12:48:00,1.7 +W-201,2026-03-06 13:00:00,2.0 +W-202,2026-03-06 13:03:00,0.8 +W-201,2026-03-06 13:15:00,1.1 +W-202,2026-03-06 13:18:00,2.2 +W-201,2026-03-06 13:30:00,2.5 +W-202,2026-03-06 13:33:00,1.3 +W-201,2026-03-06 13:45:00,1.6 +W-202,2026-03-06 13:48:00,0.4 +W-201,2026-03-06 14:00:00,0.7 +W-202,2026-03-06 14:03:00,1.8 +W-201,2026-03-06 14:15:00,2.1 +W-202,2026-03-06 14:18:00,0.9 +W-201,2026-03-06 14:30:00,1.2 +W-202,2026-03-06 14:33:00,2.3 +W-201,2026-03-06 14:45:00,0.3 +W-202,2026-03-06 14:48:00,1.4 +W-201,2026-03-06 15:00:00,1.7 +W-202,2026-03-06 15:03:00,0.5 +W-201,2026-03-06 15:15:00,0.8 +W-202,2026-03-06 15:18:00,1.9 +W-201,2026-03-06 15:30:00,2.2 +W-202,2026-03-06 15:33:00,1.0 +W-201,2026-03-06 15:45:00,1.3 +W-202,2026-03-06 15:48:00,2.4 +W-201,2026-03-06 16:00:00,0.4 +W-202,2026-03-06 16:03:00,1.5 +W-201,2026-03-06 16:15:00,1.8 +W-202,2026-03-06 16:18:00,0.6 +W-201,2026-03-06 16:30:00,0.9 +W-202,2026-03-06 16:33:00,2.0 +W-201,2026-03-06 16:45:00,2.3 +W-202,2026-03-06 16:48:00,1.1 +W-201,2026-03-06 17:00:00,1.4 +W-202,2026-03-06 17:03:00,2.5 +W-201,2026-03-06 17:15:00,0.5 +W-202,2026-03-06 17:18:00,1.6 +W-201,2026-03-06 17:30:00,1.9 +W-202,2026-03-06 17:33:00,0.7 +W-201,2026-03-06 17:45:00,1.0 +W-202,2026-03-06 17:48:00,2.1 +W-201,2026-03-06 18:00:00,2.4 +W-202,2026-03-06 18:03:00,1.2 +W-201,2026-03-06 18:15:00,1.5 +W-202,2026-03-06 18:18:00,0.3 +W-201,2026-03-06 18:30:00,0.6 +W-202,2026-03-06 18:33:00,1.7 +W-201,2026-03-06 18:45:00,2.0 +W-202,2026-03-06 18:48:00,0.8 +W-201,2026-03-06 19:00:00,1.1 +W-202,2026-03-06 19:03:00,2.2 +W-201,2026-03-06 19:15:00,2.5 +W-202,2026-03-06 19:18:00,1.3 +W-201,2026-03-06 19:30:00,1.6 +W-202,2026-03-06 19:33:00,0.4 +W-201,2026-03-06 19:45:00,0.7 +W-202,2026-03-06 19:48:00,1.8 +W-201,2026-03-06 20:00:00,2.1 +W-202,2026-03-06 20:03:00,0.9 +W-201,2026-03-06 20:15:00,1.2 +W-202,2026-03-06 20:18:00,2.3 +W-201,2026-03-06 20:30:00,0.3 +W-202,2026-03-06 20:33:00,1.4 +W-201,2026-03-06 20:45:00,1.7 +W-202,2026-03-06 20:48:00,0.5 +W-201,2026-03-06 21:00:00,0.8 +W-202,2026-03-06 21:03:00,1.9 +W-201,2026-03-06 21:15:00,2.2 +W-202,2026-03-06 21:18:00,1.0 +W-201,2026-03-06 21:30:00,1.3 +W-202,2026-03-06 21:33:00,2.4 +W-201,2026-03-06 21:45:00,0.4 +W-202,2026-03-06 21:48:00,1.5 +W-201,2026-03-06 22:00:00,1.8 +W-202,2026-03-06 22:03:00,0.6 +W-201,2026-03-06 22:15:00,0.9 +W-202,2026-03-06 22:18:00,2.0 +W-201,2026-03-06 22:30:00,2.3 +W-202,2026-03-06 22:33:00,1.1 +W-201,2026-03-06 22:45:00,1.4 +W-202,2026-03-06 22:48:00,2.5 +W-201,2026-03-06 23:00:00,0.5 +W-202,2026-03-06 23:03:00,1.6 +W-201,2026-03-06 23:15:00,1.9 +W-202,2026-03-06 23:18:00,0.7 +W-201,2026-03-06 23:30:00,1.0 +W-202,2026-03-06 23:33:00,2.1 +W-201,2026-03-06 23:45:00,2.4 +W-202,2026-03-06 23:48:00,1.2 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/tariffs.csv b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/tariffs.csv new file mode 100644 index 0000000..ab3c9be --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/data/tariffs.csv @@ -0,0 +1,13 @@ +day,band,cents_per_kwh +2026-03-03,offpeak,11 +2026-03-03,peak,28 +2026-03-04,offpeak,12 +2026-03-04,peak,30 +2026-03-05,offpeak,13 +2026-03-05,peak,27 +2026-03-06,offpeak,11 +2026-03-06,peak,29 +2026-03-07,offpeak,12 +2026-03-07,peak,31 +2026-03-08,offpeak,13 +2026-03-08,peak,28 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/__init__.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/__init__.py new file mode 100644 index 0000000..4e98540 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/__init__.py @@ -0,0 +1 @@ +"""Shared code for the metering DAGs (on sys.path via the plugins folder).""" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/assets.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/assets.py new file mode 100644 index 0000000..4660d32 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/assets.py @@ -0,0 +1,14 @@ +"""Datasets and paths shared by the metering DAGs.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from airflow.datasets import Dataset + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +LAKE = PROJECT_ROOT / "output" / "lake" +REGIONS: list[str] = json.loads((PROJECT_ROOT / "config" / "regions.json").read_text())["regions"] + +READINGS = {region: Dataset(f"lake://metering/readings/{region}") for region in REGIONS} diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/operators.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/operators.py new file mode 100644 index 0000000..6a97a3e --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/operators.py @@ -0,0 +1,56 @@ +"""Operators shared by the metering DAGs.""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.models import BaseOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +class CsvSliceOperator(BaseOperator): + """Copy the rows of ``source`` whose ``ts_column`` (UTC) falls in [start, end) to ``target``. + + Paths are relative to the project root. Each outlet dataset gets an event + whose ``extra`` records how many rows were landed. + """ + + template_fields = ("source", "target", "start", "end") + ui_color = "#e8f4ea" + + def __init__( + self, + *, + source: str, + target: str, + ts_column: str, + start: str = "{{ data_interval_start }}", + end: str = "{{ data_interval_end }}", + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.source = source + self.target = target + self.ts_column = ts_column + self.start = start + self.end = end + + def execute(self, context): + lo, hi = pendulum.parse(self.start), pendulum.parse(self.end) + with (PROJECT_ROOT / self.source).open(newline="") as fh: + reader = csv.DictReader(fh) + fields = reader.fieldnames + rows = [r for r in reader if lo <= pendulum.parse(r[self.ts_column], tz="UTC") < hi] + target = PROJECT_ROOT / self.target + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + for dataset in self.outlets: + context["outlet_events"][dataset.uri].extra = {"rows": len(rows)} + self.log.info("Landed %d rows from %s into %s", len(rows), self.source, self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/tariffs.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/tariffs.py new file mode 100644 index 0000000..d32a0a7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_lib/tariffs.py @@ -0,0 +1,15 @@ +"""Tariff helpers, also exposed to templates as ``macros.meter.*``.""" + +from __future__ import annotations + +PEAK_HOURS = range(16, 21) # 16:00-20:59 UTC + + +def tariff_band(hour: int) -> str: + """Tariff band of a UTC hour of day.""" + return "peak" if int(hour) in PEAK_HOURS else "offpeak" + + +def invoice_batch(day: str) -> str: + """Invoice batch id finance expects for a billing day (YYYY-MM-DD).""" + return "INV-" + str(day).replace("-", "") diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_plugin.py b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_plugin.py new file mode 100644 index 0000000..f6e9a78 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/plugins/meter_plugin.py @@ -0,0 +1,10 @@ +"""Registers the metering template macros (``macros.meter.*``).""" + +from airflow.plugins_manager import AirflowPlugin + +from meter_lib.tariffs import invoice_batch, tariff_band + + +class MeterPlugin(AirflowPlugin): + name = "meter" + macros = [invoice_batch, tariff_band] diff --git a/evals/airflow/cases/hard-migration-meter-rollups/fixture/requirements.txt b/evals/airflow/cases/hard-migration-meter-rollups/fixture/requirements.txt new file mode 100644 index 0000000..7056ae9 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==2.11.2 +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-migration-meter-rollups/grade.py b/evals/airflow/cases/hard-migration-meter-rollups/grade.py new file mode 100644 index 0000000..b8228fd --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/grade.py @@ -0,0 +1,76 @@ +"""Grader for hard-migration-meter-rollups. + +The fixture is an Airflow 2.11 metering project: two generated hourly regional +loaders (custom plugin operator, dataset outlets), a rollup scheduled on +``east & west`` that works on its run's data interval and reads +``triggering_dataset_events``, a daily tariff sync and daily billing driven by +``.sh`` templates (removed context keys, ``conf`` and a plugin macro), and a +registry export. + +The original project is replayed in the 2.11 env and the agent's project in the +3.3 env with the same sequence of scheduled runs, scheduler-style +dataset/asset-triggered runs (batched events), and CLI triggers without a +logical date (see ``hardsim``). Task code runs with the Airflow 3 worker's +metadata-DB block emulated. Every file under ``output/`` must match. + +What only shows at runtime on 3.3: asset-triggered runs have no data interval +(2.x derived one from the triggering events' source runs), the triggering and +outlet event accessors no longer accept URI strings, ``conf`` is gone from +the template context, ``tomorrow_ds``/``execution_date`` are gone, CLI triggers +have no ``ds``/``data_interval_*``, and bare cron/preset schedules become +trigger timetables. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import hardsim as hs # noqa: E402 + +DAG_SHAPES = { + "readings_east_hourly": ({"land_readings"}, []), + "readings_west_hourly": ({"land_readings"}, []), + "usage_rollup": ({"rollup"}, []), + "tariffs_daily": ({"sync_tariffs"}, []), + "billing_daily": ({"bill", "publish_invoices"}, [["bill", "publish_invoices"]]), + "meter_registry_daily": ({"export_registry"}, []), +} +D = "2026-03-05T{}:00:00+00:00" +STEPS = [ + ["tariffs_daily", "2026-03-05T01:00:00+00:00"], + ["tariffs_daily", "2026-03-06T01:00:00+00:00"], + ["meter_registry_daily", "2026-03-06T00:00:00+00:00"], + ["readings_east_hourly", D.format("01")], + ["readings_east_hourly", D.format("02")], + ["readings_west_hourly", D.format("01")], + ["usage_rollup", hs.asset_trigger("2026-03-05T02:10:00+00:00")], + ["readings_west_hourly", D.format("02")], + ["readings_west_hourly", D.format("03")], + ["readings_east_hourly", D.format("03")], + ["usage_rollup", hs.asset_trigger("2026-03-05T03:10:00+00:00")], + ["billing_daily", "2026-03-06T02:30:00+00:00"], + ["readings_east_hourly", D.format("04")], + ["readings_west_hourly", D.format("04")], + ["usage_rollup", hs.asset_trigger("2026-03-05T04:10:00+00:00")], + ["tariffs_daily", hs.trigger("2026-03-06T09:20:00+00:00")], + ["billing_daily", hs.trigger("2026-03-07T01:15:00+00:00")], +] +AREAS = [ + ("hourly regional loads land the same readings files", ("lake/readings/",)), + ("tariff syncs (scheduled and the CLI-triggered correction) write the same files", ("lake/tariffs/",)), + ("asset-triggered rollups cover the same hours with the same manifests", ("rollup/",)), + ("billing and invoice publication (scheduled and the CLI-triggered re-bill) match", ("billing/", "dropbox/")), + ("registry snapshot matches", ("registry/",)), +] + + +def env_extra(ws: Path) -> dict: + return {"AIRFLOW__CORE__PLUGINS_FOLDER": str(ws / "plugins"), + "AIRFLOW__METERING__INVOICE_DROPBOX": str(ws / "output" / "dropbox")} + + +if __name__ == "__main__": + hs.grade_project(CASE_DIR, dag_shapes=DAG_SHAPES, steps=STEPS, areas=AREAS, env_extra=env_extra) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/hardsim.py b/evals/airflow/cases/hard-migration-meter-rollups/hardsim.py new file mode 100644 index 0000000..bd0713b --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/hardsim.py @@ -0,0 +1,658 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +Extended for the ``hard-migration-*`` cases (identical in each of them): CLI/API +triggers without a logical date (``trigger``), scheduler-style asset/dataset +triggered runs that consume the recorded events (``asset_trigger``), and +multi-DAG replays against one metadata DB in one process (``replay``). +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + # 2.x dag.test() does not write DAGs to the DB; dataset references + # (needed to record and consume dataset events) come from this sync. + if not bag.import_errors: + bag.sync_to_db() + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +def _run_state(dr_id, session_factory): + from airflow.models.dagrun import DagRun + with session_factory() as s: + dr = s.get(DagRun, dr_id) + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + + +def run_asset_triggered(dag, run_after, row): + """Run ``dag`` the way the scheduler runs an asset/dataset-triggered DagRun at ``run_after``. + + Consumed events: every event of an asset the DAG is scheduled on that no earlier + asset-triggered run of this DAG consumed (the grader triggers only once the DAG's + condition is met). 3.x: logical_date and data_interval are None (scheduler code). + 2.x: execution_date = run_after and the interval comes from + ``timetable.data_interval_for_events`` (scheduler code).""" + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + created = {} + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.models.asset import AssetEvent, DagScheduleAssetReference + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, session, **_): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.ASSET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_asset_events} + from sqlalchemy import or_ + from airflow.models.asset import AssetAliasModel, DagScheduleAssetAliasReference + asset_ids = [r.asset_id for r in session.query(DagScheduleAssetReference) + .filter(DagScheduleAssetReference.dag_id == dag.dag_id).all()] + via_alias = AssetEvent.source_aliases.any(AssetAliasModel.scheduled_dags.any( + DagScheduleAssetAliasReference.dag_id == dag.dag_id)) + events = [e for e in session.query(AssetEvent).filter(or_(AssetEvent.asset_id.in_(asset_ids), via_alias)) + .order_by(AssetEvent.timestamp, AssetEvent.id).all() if e.id not in consumed] + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=DagRunType.ASSET_TRIGGERED, logical_date=None, + run_after=run_after), + logical_date=None, data_interval=None, run_after=run_after, + run_type=DagRunType.ASSET_TRIGGERED, triggered_by=DagRunTriggeredByType.ASSET, + state=DagRunState.RUNNING, start_date=run_after, session=session, conf=None) + dr.consumed_asset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + drmod.get_or_create_dagrun = patched + try: + dag.test(logical_date=None, run_after=run_after) + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + from airflow.models.dataset import DagScheduleDatasetReference, DatasetEvent + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + prev = session.query(DagRun).filter(DagRun.dag_id == dag.dag_id, + DagRun.run_type == DagRunType.DATASET_TRIGGERED).all() + consumed = {e.id for r in prev for e in r.consumed_dataset_events} + ds_ids = [r.dataset_id for r in session.query(DagScheduleDatasetReference) + .filter(DagScheduleDatasetReference.dag_id == dag.dag_id).all()] + events = [e for e in session.query(DatasetEvent).filter(DatasetEvent.dataset_id.in_(ds_ids)) + .order_by(DatasetEvent.timestamp, DatasetEvent.id).all() if e.id not in consumed] + interval = dag.timetable.data_interval_for_events(execution_date, events) + dr = dag.create_dagrun( + run_id=DagRun.generate_run_id(DagRunType.DATASET_TRIGGERED, execution_date), + run_type=DagRunType.DATASET_TRIGGERED, execution_date=execution_date, + data_interval=interval, state=DagRunState.RUNNING, start_date=execution_date, + session=session, external_trigger=False) + dr.consumed_dataset_events.extend(events) + session.commit() + created["id"], created["events"] = dr.id, len(events) + return dr + + dagmod._get_or_create_dagrun = patched + try: + dag.test(execution_date=run_after) + finally: + dagmod._get_or_create_dagrun = orig + row["events"] = created.get("events") + if "id" not in created: + row["error"] = "asset-triggered DagRun was not created" + return None + row["state"], row["tasks"] = _run_state(created["id"], create_session) + return created["id"] + + +import contextlib + + +@contextlib.contextmanager +def run_kind(kind): + """Make dag.test() create the DagRun the scheduler (kind="scheduled") or a CLI + trigger (kind="trigger") would create: run_type, run_id format, triggered_by + (3.x) / external_trigger (2.x). dag.test() alone makes every run MANUAL.""" + from airflow.models.dagrun import DagRun + from airflow.utils.state import DagRunState + from airflow.utils.types import DagRunType + rtype = DagRunType.SCHEDULED if kind == "scheduled" else DagRunType.MANUAL + if MAJOR >= 3: + from airflow.models import dagrun as drmod + from airflow.utils.types import DagRunTriggeredByType + orig = drmod.get_or_create_dagrun + + def patched(*, dag, run_id, logical_date, data_interval, run_after, conf, session, start_date, **_): + from sqlalchemy import select + if logical_date is not None: + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.logical_date == logical_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(run_type=rtype, logical_date=logical_date, run_after=run_after), + logical_date=logical_date, data_interval=data_interval, run_after=run_after, conf=conf, + run_type=rtype, + triggered_by=DagRunTriggeredByType.TIMETABLE if kind == "scheduled" else DagRunTriggeredByType.CLI, + state=DagRunState.RUNNING, start_date=start_date or run_after, session=session) + + drmod.get_or_create_dagrun = patched + try: + yield + finally: + drmod.get_or_create_dagrun = orig + else: + from airflow.models import dag as dagmod + orig = dagmod._get_or_create_dagrun + + def patched(dag, conf, start_date, execution_date, run_id, session, data_interval=None): + from sqlalchemy import select + old = session.scalar(select(DagRun).where(DagRun.dag_id == dag.dag_id, + DagRun.execution_date == execution_date)) + if old: + session.delete(old) + session.commit() + return dag.create_dagrun( + run_id=DagRun.generate_run_id(rtype, execution_date), run_type=rtype, + execution_date=execution_date, data_interval=data_interval, state=DagRunState.RUNNING, + start_date=start_date or execution_date, session=session, conf=conf, + external_trigger=(kind == "trigger")) + + dagmod._get_or_create_dagrun = patched + try: + yield + finally: + dagmod._get_or_create_dagrun = orig + + +_DAGS = {} + + +def get_dag_tt(dag_id): + if dag_id not in _DAGS: + dag = load_dag(dag_id) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + _DAGS[dag_id] = (dag, core_timetable(dag)) + return _DAGS[dag_id] + + +def run_step(dag_id, spec): + dag, tt = get_dag_tt(dag_id) + RESULT.setdefault("timetables", {})[dag_id] = type(tt).__name__ + if isinstance(spec, dict) and "asset_trigger" in spec: + run_after = pendulum.parse(spec["asset_trigger"]) + row = {"dag_id": dag_id, "asset_trigger": spec["asset_trigger"]} + RESULT["runs"].append(row) + try: + run_asset_triggered(dag, run_after, row) + except Exception as exc: # noqa: BLE001 + import traceback + row["error"] = f"asset-triggered run raised {type(exc).__name__}: {exc}\n" + traceback.format_exc(limit=4) + return + if isinstance(spec, dict) and "trigger" in spec: + # CLI/API trigger (no logical date) at wall-clock T. 3.x: no logical date, run_after=T. + # 2.x: logical date = T (the trigger time), interval inferred from T. + run_after = pendulum.parse(spec["trigger"]) + row = {"dag_id": dag_id, "trigger": spec["trigger"]} + RESULT["runs"].append(row) + try: + with run_kind("trigger"): + if MAJOR >= 3: + dag.test(logical_date=None, run_after=run_after, run_conf=spec.get("conf")) + else: + dag.test(execution_date=run_after, run_conf=spec.get("conf")) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + if MAJOR >= 3: + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + with create_session() as s: + drs = s.query(DagRun).filter(DagRun.dag_id == dag_id, DagRun.run_after == run_after, + DagRun.logical_date.is_(None)).all() + if len(drs) == 1: + row["state"] = str(getattr(drs[0].state, "value", drs[0].state)) + row["tasks"] = {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in drs[0].get_task_instances(session=s)} + else: + row["state"], row["tasks"] = None, {} + row.setdefault("error", f"found {len(drs)} runs with run_after={run_after}") + else: + row["state"], row["tasks"] = task_states(dag_id, run_after) + forgive_deadline_bug(row) + return + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"dag_id": dag_id, "manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(dag_id, logical) + forgive_deadline_bug(row) + return + run_after = spec + row = {"dag_id": dag_id, "run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {dag_id} fires at {run_after} ({type(tt).__name__})" + return + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + with run_kind("scheduled"): + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(dag_id, info.logical_date) + forgive_deadline_bug(row) + + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + for dag_id, spec in P["steps"]: + try: + run_step(dag_id, spec) + except Exception as exc: # noqa: BLE001 (e.g. DAG missing): record, keep going + import traceback + RESULT["runs"].append({"dag_id": dag_id, "spec": spec, + "error": traceback.format_exc(limit=4)[-1500:]}) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is an ISO datetime T (the *scheduled* run that fires at T, with + the logical date and data interval the DAG's timetable gives it), ``trigger(T)`` (a + CLI/API trigger without a logical date at T), ``asset_trigger(T)`` (the scheduler's asset/dataset-triggered + run at T) or ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + return replay(env_py, workspace, [[dag_id, r] for r in runs], env=env, block_orm=block_orm, + timeout=timeout) + + +def replay(env_py: str, workspace: Path, steps: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 900) -> tuple[dict, str]: + """Like :func:`simulate`, for ``steps = [[dag_id, run_spec], ...]`` across DAGs, in one process.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), + "steps": [list(s) for s in steps], "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def asset_trigger(run_after: str) -> dict: + """Run spec: the asset/dataset-triggered run the scheduler creates at ``run_after``, + consuming every event of the DAG's assets not consumed by an earlier such run.""" + return {"asset_trigger": run_after} + + +def trigger(run_after: str, conf: dict | None = None) -> dict: + """Run spec: a CLI/API trigger (no logical date) at wall-clock ``run_after`` with no logical date + (3.x: ``dag.test(logical_date=None, run_after=T)``; 2.x: logical date = T), + optionally "with config" (``dag_run.conf``).""" + return {"trigger": run_after, "conf": conf} + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + def label(r): + for key, name in (("run_after", "scheduled"), ("trigger", "CLI trigger"), + ("asset_trigger", "asset-triggered"), ("manual", "dags test")): + if r.get(key): + return f"{r.get('dag_id', '')} {name} {r[key]}".strip() + return f"{r.get('dag_id', '')} {r.get('spec', '?')}" + return "; ".join( + f"{label(r)}: " + (str(r.get("error"))[-1200:] if r.get("error") else + f"state={r.get('state')} failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log + + +# --------------------------------------------------------------------------- +# Whole-project comparison grading shared by the hard-migration-* cases. + +_SCHEDULE_PROBE = r""" +from airflow.timetables.base import TimeRestriction + +def cond(obj): + if "Alias" in type(obj).__name__: # 2.x: _DatasetAliasCondition (an "any" of resolved datasets) + return "alias:" + obj.name + if hasattr(obj, "objects"): + kind = "all" if "All" in type(obj).__name__ else "any" + parts = [] + for o in obj.objects: # flatten nested groups of the same kind + c = cond(o) + parts.extend(c[kind] if isinstance(c, dict) and kind in c else [c]) + if len(parts) == 1: # [x], x, any(x) and all(x) are the same condition + return parts[0] + return {kind: sorted(parts, key=repr)} + return getattr(obj, "uri", None) or repr(obj) + +out = {} +for dag_id in DAG_IDS: + try: + dag = get_dag(dag_id) + except Exception as exc: + out[dag_id] = {"error": repr(exc)[:300]} + continue + tt = core_timetable(dag) + c = None + if type(tt).__name__ in ("AssetTriggeredTimetable", "DatasetTriggeredTimetable"): + c = getattr(tt, "asset_condition", None) or getattr(tt, "dataset_condition", None) + row = {"catchup": bool(dag.catchup), "timetable_kind": "assets" if c is not None else "time"} + if c is not None: + row["condition"] = cond(c) + else: + last, runs = None, [] + r = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + for _ in range(N_RUNS): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=r) + if info is None: + break + runs.append([info.run_after.isoformat(), info.data_interval.start.isoformat(), + info.data_interval.end.isoformat()]) + last = info.data_interval + row["first_runs"] = runs + out[dag_id] = row +RESULT = out +""" + + +def schedules(env_py: str, workspace: Path, dag_ids, env: dict, n_runs: int = 6) -> tuple[dict | None, str]: + """Per DAG: catchup, and either the asset condition or the first ``n_runs`` scheduled + (run_after, interval start, interval end) from start_date (catchup restriction).""" + code = f"DAG_IDS = {sorted(dag_ids)!r}\nN_RUNS = {n_runs}\n" + _SCHEDULE_PROBE + g.ensure_db(env_py, env) # 2.x resolves dataset aliases against the DB while parsing + res, proc = g.probe_json(env_py, code, workspace, env=env) + return res, proc.tail(20) + + +def _diff_detail(exp: dict[str, str], have: dict[str, str]) -> str: + if exp == have: + return "" + missing = sorted(set(exp) - set(have)) + extra = sorted(set(have) - set(exp)) + changed = sorted(k for k in set(exp) & set(have) if exp[k] != have[k]) + detail = f"missing={missing} unexpected={extra} changed={changed}" + for k in changed[:2]: + detail += f"\n--- {k} expected:\n{exp[k][:700]}\n--- got:\n{have[k][:700]}" + return detail + + +def grade_project(case_dir: Path, *, dag_shapes: dict, steps: list, areas: list, env_extra, + n_schedule_runs: int = 6) -> None: + """Standard grader: original replayed in 2.11 vs the agent's project replayed in 3.3. + + ``env_extra(ws) -> dict`` adds deployment settings (plugins folder, custom config) + for both runs. ``areas = [(check name, (output path prefixes...)), ...]``.""" + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + def env_for(env_py, root): + return g.airflow_env(root, env_py=env_py, extra=env_extra(Path(root))) + + orig_ws = original_workspace(case_dir) + py2 = g.env_python("2.11") + exp_sched, log = schedules(py2, orig_ws, dag_shapes, env_for(py2, orig_ws), n_schedule_runs) + if not exp_sched or any("error" in v for v in exp_sched.values()): + raise SystemExit(f"case bug: schedule probe failed on the original: {exp_sched}\n{log}") + shutil.rmtree(orig_ws / "output", ignore_errors=True) + orig, log = replay(py2, orig_ws, steps, env=env_for(py2, orig_ws), timeout=1500) + if not runs_ok(orig): + raise SystemExit(f"case bug: original failed in 2.11: {describe(orig)}\n{log}") + expected = snapshot(orig_ws) + + imp = g.import_dags(py, ws, env=env_for(py, ws)) + dags = imp["dags"] + grader.primary(f"all {len(dag_shapes)} DAGs import on Airflow 3.3 (dags/ + plugins/)", + imp["ok"] and not imp["import_errors"] and set(dag_shapes) <= set(dags), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"missing: {sorted(set(dag_shapes) - set(dags))}") + def keeps_shape(d, tasks, deps): + # Original task ids and ordering are kept; added helper tasks are allowed as long + # as every original dependency still holds (downstream reachable from upstream). + if d not in dags or not set(tasks) <= set(dags[d]["tasks"]): + return False + down: dict[str, set] = {} + for up, dn in dags[d]["deps"]: + down.setdefault(up, set()).add(dn) + + def reach(a, b): + seen, todo = set(), [a] + while todo: + n = todo.pop() + if n == b: + return True + if n not in seen: + seen.add(n) + todo.extend(down.get(n, ())) + return False + return all(reach(up, dn) for up, dn in deps) + + shape_ok = all(keeps_shape(d, t, deps) for d, (t, deps) in dag_shapes.items()) + grader.primary("every DAG keeps its tasks and dependencies (added helper tasks allowed)", shape_ok, + "; ".join(f"{d}: tasks={dags[d]['tasks']} deps={dags[d]['deps']}" if d in dags + else f"{d}: missing" for d in dag_shapes)) + + got_sched, log = schedules(py, ws, dag_shapes, env_for(py, ws), n_schedule_runs) + diffs = [f"{d}: expected {exp_sched[d]}\n got {(got_sched or {}).get(d)}" + for d in sorted(dag_shapes) if (got_sched or {}).get(d) != exp_sched[d]] + grader.primary("schedules unchanged: same scheduled runs and data intervals, catchup, asset conditions", + got_sched is not None and not diffs, "\n".join(diffs) or ("" if got_sched else log)) + + shutil.rmtree(ws / "output", ignore_errors=True) + got, log = replay(py, ws, steps, env=env_for(py, ws), block_orm=True, timeout=1500) + ok = runs_ok(got) + grader.primary("every replayed run (scheduled, asset-triggered, CLI-triggered) succeeds on 3.3", ok, + describe(got) + ("" if ok else "\n" + log[-2500:])) + snap = snapshot(ws) + for name, prefixes in areas: + exp = {k: v for k, v in expected.items() if k.startswith(prefixes)} + have = {k: v for k, v in snap.items() if k.startswith(prefixes)} + grader.primary(name, exp == have, _diff_detail(exp, have)) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/billing_trigger_previous_calendar_day/dags/billing_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/billing_trigger_previous_calendar_day/dags/billing_daily.py new file mode 100644 index 0000000..ccd76a7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/billing_trigger_previous_calendar_day/dags/billing_daily.py @@ -0,0 +1,83 @@ +"""Daily billing of the previous UTC day, then publication of the invoice batch. + +``bill`` prices every metered hour of the day (latest rollup wins for an hour) +with that day's tariffs and writes output/billing/.csv. ``publish_invoices`` +copies it into the finance dropbox (``[metering] invoice_dropbox``) as the +invoice batch file. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import CronDataIntervalTimetable, conf, dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT +from meter_lib.tariffs import tariff_band + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" +BILLING_DIR = PROJECT_ROOT / "output" / "billing" +CRON = "30 2 * * *" + + +def billing_day(data_interval_start, run_after) -> str: + """Day billed by a run: its interval's day, or for a CLI trigger (no interval on + Airflow 3) the latest closed interval before the trigger, as Airflow 2 did.""" + if data_interval_start is None: + data_interval_start = pendulum.instance(run_after).in_tz("UTC").subtract(days=1) + return pendulum.instance(data_interval_start).strftime("%Y-%m-%d") + + +@dag( + schedule=CronDataIntervalTimetable(CRON, timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + user_defined_macros={"conf": conf}, + default_args={"owner": "metering-finance", "retries": 1}, + tags=["metering", "billing"], +) +def billing_daily(): + @task + def bill(data_interval_start=None, dag_run=None) -> str: + day = billing_day(data_interval_start, dag_run.run_after) + rates = {} + with (LAKE / "tariffs" / f"{day}.csv").open() as fh: + for line in fh: + _, band, cents = line.strip().split(",") + rates[band] = int(cents) + + usage: dict[tuple[str, str], float] = {} + for path in sorted(ROLLUP_DIR.glob("usage_*.csv")): # later rollups win + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["hour"].startswith(day): + usage[(row["meter_id"], row["hour"])] = float(row["kwh"]) + + totals: dict[tuple[str, str], float] = defaultdict(float) + for (meter, hour), kwh in usage.items(): + totals[(meter, tariff_band(int(hour[11:13])))] += kwh + + BILLING_DIR.mkdir(parents=True, exist_ok=True) + with (BILLING_DIR / f"{day}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "band", "kwh", "amount_cents"]) + for (meter, band), kwh in sorted(totals.items()): + writer.writerow([meter, band, f"{kwh:.1f}", round(kwh * rates[band])]) + return day + + publish = BashOperator( + task_id="publish_invoices", + bash_command="publish_invoices.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) + bill() >> publish + + +billing_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/manifest_rows_from_files/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/manifest_rows_from_files/dags/usage_rollup.py new file mode 100644 index 0000000..601eed7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/manifest_rows_from_files/dags/usage_rollup.py @@ -0,0 +1,87 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.sdk import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +def triggered_span(triggering_asset_events): + """[start, end) from the earliest source-run interval start to the latest source-run + interval end of the triggering events (what Airflow 2 gave asset-triggered runs as + their data interval; Airflow 3 gives them none).""" + starts, ends = [], [] + for events in triggering_asset_events.values(): + for event in events: + run = event.source_dag_run + if run is not None and run.data_interval_start is not None: + starts.append(pendulum.instance(run.data_interval_start)) + ends.append(pendulum.instance(run.data_interval_end)) + else: + starts.append(pendulum.instance(event.timestamp)) + ends.append(pendulum.instance(event.timestamp)) + return min(starts), max(ends) + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(triggering_asset_events=None) -> str: + start, end = triggered_span(triggering_asset_events) + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum( + sum(1 for _ in csv.DictReader(p.open(newline=""))) + for p in (LAKE / "readings" / region).glob("*.csv") + if start <= pendulum.from_format(p.stem, "YYYYMMDDTHH", tz="UTC") < end) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/outlet_events_uri_key/plugins/meter_lib/operators.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/outlet_events_uri_key/plugins/meter_lib/operators.py new file mode 100644 index 0000000..401da93 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/outlet_events_uri_key/plugins/meter_lib/operators.py @@ -0,0 +1,56 @@ +"""Operators shared by the metering DAGs.""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import BaseOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +class CsvSliceOperator(BaseOperator): + """Copy the rows of ``source`` whose ``ts_column`` (UTC) falls in [start, end) to ``target``. + + Paths are relative to the project root. Each outlet asset gets an event + whose ``extra`` records how many rows were landed. + """ + + template_fields = ("source", "target", "start", "end") + ui_color = "#e8f4ea" + + def __init__( + self, + *, + source: str, + target: str, + ts_column: str, + start: str = "{{ data_interval_start }}", + end: str = "{{ data_interval_end }}", + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.source = source + self.target = target + self.ts_column = ts_column + self.start = start + self.end = end + + def execute(self, context): + lo, hi = pendulum.parse(self.start), pendulum.parse(self.end) + with (PROJECT_ROOT / self.source).open(newline="") as fh: + reader = csv.DictReader(fh) + fields = reader.fieldnames + rows = [r for r in reader if lo <= pendulum.parse(r[self.ts_column], tz="UTC") < hi] + target = PROJECT_ROOT / self.target + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + for asset in self.outlets: + context["outlet_events"][asset.uri].extra = {"rows": len(rows)} + self.log.info("Landed %d rows from %s into %s", len(rows), self.source, self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/publish_conf_missing/dags/billing_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/publish_conf_missing/dags/billing_daily.py new file mode 100644 index 0000000..79051cb --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/publish_conf_missing/dags/billing_daily.py @@ -0,0 +1,83 @@ +"""Daily billing of the previous UTC day, then publication of the invoice batch. + +``bill`` prices every metered hour of the day (latest rollup wins for an hour) +with that day's tariffs and writes output/billing/.csv. ``publish_invoices`` +copies it into the finance dropbox (``[metering] invoice_dropbox``) as the +invoice batch file. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import CronDataIntervalTimetable, conf, dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT +from meter_lib.tariffs import tariff_band + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" +BILLING_DIR = PROJECT_ROOT / "output" / "billing" +CRON = "30 2 * * *" + + +def billing_day(data_interval_start, run_after) -> str: + """Day billed by a run: its interval's day, or for a CLI trigger (no interval on + Airflow 3) the latest closed interval before the trigger, as Airflow 2 did.""" + if data_interval_start is None: + closed_end = pendulum.instance(run_after).in_tz("UTC").subtract(hours=2, minutes=30).start_of("day") + data_interval_start = closed_end.add(hours=2, minutes=30).subtract(days=1) + return pendulum.instance(data_interval_start).strftime("%Y-%m-%d") + + +@dag( + schedule=CronDataIntervalTimetable(CRON, timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + default_args={"owner": "metering-finance", "retries": 1}, + tags=["metering", "billing"], +) +def billing_daily(): + @task + def bill(data_interval_start=None, dag_run=None) -> str: + day = billing_day(data_interval_start, dag_run.run_after) + rates = {} + with (LAKE / "tariffs" / f"{day}.csv").open() as fh: + for line in fh: + _, band, cents = line.strip().split(",") + rates[band] = int(cents) + + usage: dict[tuple[str, str], float] = {} + for path in sorted(ROLLUP_DIR.glob("usage_*.csv")): # later rollups win + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["hour"].startswith(day): + usage[(row["meter_id"], row["hour"])] = float(row["kwh"]) + + totals: dict[tuple[str, str], float] = defaultdict(float) + for (meter, hour), kwh in usage.items(): + totals[(meter, tariff_band(int(hour[11:13])))] += kwh + + BILLING_DIR.mkdir(parents=True, exist_ok=True) + with (BILLING_DIR / f"{day}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "band", "kwh", "amount_cents"]) + for (meter, band), kwh in sorted(totals.items()): + writer.writerow([meter, band, f"{kwh:.1f}", round(kwh * rates[band])]) + return day + + publish = BashOperator( + task_id="publish_invoices", + bash_command="publish_invoices.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) + bill() >> publish + + +billing_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_all_landed_hours/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_all_landed_hours/dags/usage_rollup.py new file mode 100644 index 0000000..02590c3 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_all_landed_hours/dags/usage_rollup.py @@ -0,0 +1,87 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.sdk import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +def triggered_span(triggering_asset_events): + """[start, end) from the earliest source-run interval start to the latest source-run + interval end of the triggering events (what Airflow 2 gave asset-triggered runs as + their data interval; Airflow 3 gives them none).""" + starts, ends = [], [] + for events in triggering_asset_events.values(): + for event in events: + run = event.source_dag_run + if run is not None and run.data_interval_start is not None: + starts.append(pendulum.instance(run.data_interval_start)) + ends.append(pendulum.instance(run.data_interval_end)) + else: + starts.append(pendulum.instance(event.timestamp)) + ends.append(pendulum.instance(event.timestamp)) + return min(starts), max(ends) + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(triggering_asset_events=None) -> str: + # 3.x gives asset-triggered runs no interval: roll up everything landed so far + landed = [pendulum.from_format(p.stem, "YYYYMMDDTHH", tz="UTC") + for region in REGIONS for p in (LAKE / "readings" / region).glob("*.csv")] + start, end = min(landed), max(landed).add(hours=1) + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum(event.extra.get("rows", 0) for event in triggering_asset_events.get(READINGS[region], [])) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_run_after_hour/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_run_after_hour/dags/usage_rollup.py new file mode 100644 index 0000000..025d511 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/rollup_run_after_hour/dags/usage_rollup.py @@ -0,0 +1,86 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.sdk import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +def triggered_span(triggering_asset_events): + """[start, end) from the earliest source-run interval start to the latest source-run + interval end of the triggering events (what Airflow 2 gave asset-triggered runs as + their data interval; Airflow 3 gives them none).""" + starts, ends = [], [] + for events in triggering_asset_events.values(): + for event in events: + run = event.source_dag_run + if run is not None and run.data_interval_start is not None: + starts.append(pendulum.instance(run.data_interval_start)) + ends.append(pendulum.instance(run.data_interval_end)) + else: + starts.append(pendulum.instance(event.timestamp)) + ends.append(pendulum.instance(event.timestamp)) + return min(starts), max(ends) + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(triggering_asset_events=None, dag_run=None) -> str: + # asset-triggered runs have no interval on 3.x: roll up the hour before the run + end = pendulum.instance(dag_run.run_after).start_of("hour") + start = end.subtract(hours=1) + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum(event.extra.get("rows", 0) for event in triggering_asset_events.get(READINGS[region], [])) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_bare_cron/dags/tariffs_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_bare_cron/dags/tariffs_daily.py new file mode 100644 index 0000000..6365896 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_bare_cron/dags/tariffs_daily.py @@ -0,0 +1,27 @@ +"""Daily tariff sync from the utility's price sheet (data/tariffs.csv).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + +with DAG( + dag_id="tariffs_daily", + schedule="0 1 * * *", + start_date=pendulum.datetime(2026, 3, 3, tz="UTC"), + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + default_args={"owner": "metering", "retries": 1}, + tags=["metering", "reference-data"], +) as dag: + BashOperator( + task_id="sync_tariffs", + bash_command="sync_tariffs.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_jinja_or_undefined/dags/scripts/sync_tariffs.sh b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_jinja_or_undefined/dags/scripts/sync_tariffs.sh new file mode 100644 index 0000000..0aa12a8 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_jinja_or_undefined/dags/scripts/sync_tariffs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Day-ahead prices: the sync for a day also refreshes the next day. +# Scheduled runs sync their interval's day; a CLI trigger (no logical date) syncs today. +{%- set day = (logical_date or dag_run.run_after) | ds %} +{%- set next_day = macros.ds_add(day, 1) %} +set -euo pipefail +dest="$PROJECT_ROOT/output/lake/tariffs" +mkdir -p "$dest" +for day in {{ day }} {{ next_day }}; do + grep "^${day}," "$PROJECT_ROOT/data/tariffs.csv" > "$dest/${day}.csv" +done +echo "synced tariffs for {{ day }} and {{ next_day }}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_trigger_latest_interval/dags/scripts/sync_tariffs.sh b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_trigger_latest_interval/dags/scripts/sync_tariffs.sh new file mode 100644 index 0000000..2d15802 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/mutants/tariffs_trigger_latest_interval/dags/scripts/sync_tariffs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Day-ahead prices: the sync for a day also refreshes the next day. +# Scheduled runs sync their interval's day; a CLI trigger (no logical date) syncs today. +{%- set day = ds if ds is defined else macros.ds_add(dag_run.run_after | ds, -1) %} +{%- set next_day = macros.ds_add(day, 1) %} +set -euo pipefail +dest="$PROJECT_ROOT/output/lake/tariffs" +mkdir -p "$dest" +for day in {{ day }} {{ next_day }}; do + grep "^${day}," "$PROJECT_ROOT/data/tariffs.csv" > "$dest/${day}.csv" +done +echo "synced tariffs for {{ day }} and {{ next_day }}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/billing_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/billing_daily.py new file mode 100644 index 0000000..e99bf52 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/billing_daily.py @@ -0,0 +1,84 @@ +"""Daily billing of the previous UTC day, then publication of the invoice batch. + +``bill`` prices every metered hour of the day (latest rollup wins for an hour) +with that day's tariffs and writes output/billing/.csv. ``publish_invoices`` +copies it into the finance dropbox (``[metering] invoice_dropbox``) as the +invoice batch file. +""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import CronDataIntervalTimetable, conf, dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT +from meter_lib.tariffs import tariff_band + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" +BILLING_DIR = PROJECT_ROOT / "output" / "billing" +CRON = "30 2 * * *" + + +def billing_day(data_interval_start, run_after) -> str: + """Day billed by a run: its interval's day, or for a CLI trigger (no interval on + Airflow 3) the latest closed interval before the trigger, as Airflow 2 did.""" + if data_interval_start is None: + closed_end = pendulum.instance(run_after).in_tz("UTC").subtract(hours=2, minutes=30).start_of("day") + data_interval_start = closed_end.add(hours=2, minutes=30).subtract(days=1) + return pendulum.instance(data_interval_start).strftime("%Y-%m-%d") + + +@dag( + schedule=CronDataIntervalTimetable(CRON, timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + user_defined_macros={"conf": conf}, + default_args={"owner": "metering-finance", "retries": 1}, + tags=["metering", "billing"], +) +def billing_daily(): + @task + def bill(data_interval_start=None, dag_run=None) -> str: + day = billing_day(data_interval_start, dag_run.run_after) + rates = {} + with (LAKE / "tariffs" / f"{day}.csv").open() as fh: + for line in fh: + _, band, cents = line.strip().split(",") + rates[band] = int(cents) + + usage: dict[tuple[str, str], float] = {} + for path in sorted(ROLLUP_DIR.glob("usage_*.csv")): # later rollups win + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["hour"].startswith(day): + usage[(row["meter_id"], row["hour"])] = float(row["kwh"]) + + totals: dict[tuple[str, str], float] = defaultdict(float) + for (meter, hour), kwh in usage.items(): + totals[(meter, tariff_band(int(hour[11:13])))] += kwh + + BILLING_DIR.mkdir(parents=True, exist_ok=True) + with (BILLING_DIR / f"{day}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "band", "kwh", "amount_cents"]) + for (meter, band), kwh in sorted(totals.items()): + writer.writerow([meter, band, f"{kwh:.1f}", round(kwh * rates[band])]) + return day + + publish = BashOperator( + task_id="publish_invoices", + bash_command="publish_invoices.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) + bill() >> publish + + +billing_daily() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/meter_registry.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/meter_registry.py new file mode 100644 index 0000000..745e3d7 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/meter_registry.py @@ -0,0 +1,35 @@ +"""Daily export of the meter registry snapshot for the customer portal.""" + +from __future__ import annotations + +import json + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable, Variable + +from meter_lib.assets import PROJECT_ROOT, REGIONS + +REGISTRY_DIR = PROJECT_ROOT / "output" / "registry" + + +def export_registry(**context): + version = Variable.get("registry_version", default="2026.1") + snapshot = { + "as_of": context["logical_date"].isoformat(), + "regions": REGIONS, + "registry_version": version, + } + REGISTRY_DIR.mkdir(parents=True, exist_ok=True) + (REGISTRY_DIR / f"{context['ds']}.json").write_text(json.dumps(snapshot, indent=2) + "\n") + + +with DAG( + dag_id="meter_registry_daily", + schedule=CronDataIntervalTimetable("@daily", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "metering"}, + tags=["metering", "portal"], +) as dag: + PythonOperator(task_id="export_registry", python_callable=export_registry) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/regional_readings.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/regional_readings.py new file mode 100644 index 0000000..2038f09 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/regional_readings.py @@ -0,0 +1,33 @@ +"""Hourly landing of smart-meter readings, one DAG per region (config/regions.json). + +Each run lands the readings of its hour into +output/lake/readings//.csv and updates the +region's asset, which feeds ``usage_rollup``. +""" + +from __future__ import annotations + +import pendulum +from airflow.sdk import DAG, CronDataIntervalTimetable + +from meter_lib.assets import READINGS, REGIONS +from meter_lib.operators import CsvSliceOperator + +for region in REGIONS: + with DAG( + dag_id=f"readings_{region}_hourly", + schedule=CronDataIntervalTimetable("@hourly", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering", "retries": 2}, + tags=["metering", "ingest"], + ) as dag: + CsvSliceOperator( + task_id="land_readings", + source=f"data/readings_{region}.csv", + ts_column="read_at", + target=f"output/lake/readings/{region}/" + "{{ data_interval_start.strftime('%Y%m%dT%H') }}.csv", + outlets=[READINGS[region]], + ) + globals()[dag.dag_id] = dag diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/publish_invoices.sh b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/publish_invoices.sh new file mode 100644 index 0000000..3567016 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/publish_invoices.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Publishes the billing file of the day billed by this run to the finance dropbox. +set -euo pipefail +day="{{ ti.xcom_pull(task_ids='bill') }}" +dropbox="{{ conf.get('metering', 'invoice_dropbox') }}" +mkdir -p "$dropbox" +cp "$PROJECT_ROOT/output/billing/${day}.csv" "$dropbox/{{ macros.meter.invoice_batch(ti.xcom_pull(task_ids='bill')) }}.csv" +echo "published invoices for ${day}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/sync_tariffs.sh b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/sync_tariffs.sh new file mode 100644 index 0000000..6523120 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/scripts/sync_tariffs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Day-ahead prices: the sync for a day also refreshes the next day. +# Scheduled runs sync their interval's day; a CLI trigger (no logical date) syncs today. +{%- set day = ds if ds is defined else (dag_run.run_after | ds) %} +{%- set next_day = macros.ds_add(day, 1) %} +set -euo pipefail +dest="$PROJECT_ROOT/output/lake/tariffs" +mkdir -p "$dest" +for day in {{ day }} {{ next_day }}; do + grep "^${day}," "$PROJECT_ROOT/data/tariffs.csv" > "$dest/${day}.csv" +done +echo "synced tariffs for {{ day }} and {{ next_day }}" diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/tariffs_daily.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/tariffs_daily.py new file mode 100644 index 0000000..3000e93 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/tariffs_daily.py @@ -0,0 +1,28 @@ +"""Daily tariff sync from the utility's price sheet (data/tariffs.csv).""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + +with DAG( + dag_id="tariffs_daily", + schedule=CronDataIntervalTimetable("0 1 * * *", timezone="UTC"), + catchup=True, + start_date=pendulum.datetime(2026, 3, 3, tz="UTC"), + max_active_runs=1, + template_searchpath=[str(Path(__file__).parent / "scripts")], + default_args={"owner": "metering", "retries": 1}, + tags=["metering", "reference-data"], +) as dag: + BashOperator( + task_id="sync_tariffs", + bash_command="sync_tariffs.sh", + env={"PROJECT_ROOT": str(PROJECT_ROOT)}, + append_env=True, + ) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/usage_rollup.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/usage_rollup.py new file mode 100644 index 0000000..8b1b093 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/dags/usage_rollup.py @@ -0,0 +1,84 @@ +"""Hourly usage rollup, rebuilt once every region has landed new readings. + +A run re-aggregates, for all regions, every hour from the start of the +earliest regional load that triggered it to the end of the latest one. When +several loads land close together the scheduler batches them into one run. +Output: output/rollup/usage__.csv plus a .json manifest +with the raw row counts each region delivered to this run. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict + +import pendulum +from airflow.sdk import dag, task + +from meter_lib.assets import LAKE, PROJECT_ROOT, READINGS, REGIONS + +ROLLUP_DIR = PROJECT_ROOT / "output" / "rollup" + + +def triggered_span(triggering_asset_events): + """[start, end) from the earliest source-run interval start to the latest source-run + interval end of the triggering events (what Airflow 2 gave asset-triggered runs as + their data interval; Airflow 3 gives them none).""" + starts, ends = [], [] + for events in triggering_asset_events.values(): + for event in events: + run = event.source_dag_run + if run is not None and run.data_interval_start is not None: + starts.append(pendulum.instance(run.data_interval_start)) + ends.append(pendulum.instance(run.data_interval_end)) + else: + starts.append(pendulum.instance(event.timestamp)) + ends.append(pendulum.instance(event.timestamp)) + return min(starts), max(ends) + + +@dag( + schedule=(READINGS["east"] & READINGS["west"]), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "metering"}, + tags=["metering", "rollup"], +) +def usage_rollup(): + @task + def rollup(triggering_asset_events=None) -> str: + start, end = triggered_span(triggering_asset_events) + usage: dict[tuple[str, str], float] = defaultdict(float) + hours = set() + for region in REGIONS: + for path in sorted((LAKE / "readings" / region).glob("*.csv")): + hour = pendulum.from_format(path.stem, "YYYYMMDDTHH", tz="UTC") + if not start <= hour < end: + continue + hours.add(hour.strftime("%Y-%m-%dT%H:00")) + with path.open(newline="") as fh: + for row in csv.DictReader(fh): + usage[(row["meter_id"], hour.strftime("%Y-%m-%dT%H:00"))] += float(row["kwh"]) + + name = f"usage_{start.strftime('%Y%m%dT%H')}_{end.strftime('%Y%m%dT%H')}" + ROLLUP_DIR.mkdir(parents=True, exist_ok=True) + with (ROLLUP_DIR / f"{name}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["meter_id", "hour", "kwh"]) + for (meter, hour), kwh in sorted(usage.items()): + writer.writerow([meter, hour, f"{kwh:.1f}"]) + + delivered = { + region: sum(event.extra.get("rows", 0) for event in triggering_asset_events.get(READINGS[region], [])) + for region in REGIONS + } + manifest = {"hours": sorted(hours), "rows_delivered": delivered} + (ROLLUP_DIR / f"{name}.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return name + + rollup() + + +usage_rollup() diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/assets.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/assets.py new file mode 100644 index 0000000..53ed9fa --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/assets.py @@ -0,0 +1,14 @@ +"""Datasets and paths shared by the metering DAGs.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from airflow.sdk import Asset + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +LAKE = PROJECT_ROOT / "output" / "lake" +REGIONS: list[str] = json.loads((PROJECT_ROOT / "config" / "regions.json").read_text())["regions"] + +READINGS = {region: Asset(f"lake://metering/readings/{region}") for region in REGIONS} diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/operators.py b/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/operators.py new file mode 100644 index 0000000..d4f7d51 --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/plugins/meter_lib/operators.py @@ -0,0 +1,56 @@ +"""Operators shared by the metering DAGs.""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import BaseOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +class CsvSliceOperator(BaseOperator): + """Copy the rows of ``source`` whose ``ts_column`` (UTC) falls in [start, end) to ``target``. + + Paths are relative to the project root. Each outlet asset gets an event + whose ``extra`` records how many rows were landed. + """ + + template_fields = ("source", "target", "start", "end") + ui_color = "#e8f4ea" + + def __init__( + self, + *, + source: str, + target: str, + ts_column: str, + start: str = "{{ data_interval_start }}", + end: str = "{{ data_interval_end }}", + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.source = source + self.target = target + self.ts_column = ts_column + self.start = start + self.end = end + + def execute(self, context): + lo, hi = pendulum.parse(self.start), pendulum.parse(self.end) + with (PROJECT_ROOT / self.source).open(newline="") as fh: + reader = csv.DictReader(fh) + fields = reader.fieldnames + rows = [r for r in reader if lo <= pendulum.parse(r[self.ts_column], tz="UTC") < hi] + target = PROJECT_ROOT / self.target + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + for asset in self.outlets: + context["outlet_events"][asset].extra = {"rows": len(rows)} + self.log.info("Landed %d rows from %s into %s", len(rows), self.source, self.target) + return len(rows) diff --git a/evals/airflow/cases/hard-migration-meter-rollups/reference/requirements.txt b/evals/airflow/cases/hard-migration-meter-rollups/reference/requirements.txt new file mode 100644 index 0000000..5c62f9e --- /dev/null +++ b/evals/airflow/cases/hard-migration-meter-rollups/reference/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==3.3.2 +apache-airflow-providers-standard +pendulum==3.2.0 diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/dags/pipeline_factory.py new file mode 100644 index 0000000..f0036c0 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/dags/pipeline_factory.py @@ -0,0 +1,142 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Alternative fix: every config is validated in full before any DAG object is +created (team, steps, dependencies, registered tables, cron expression), so +nothing can fail half-way through building a DAG. Problems are reported as +warnings that name the config file. The registry is parsed once per process. +""" + +from __future__ import annotations + +import hashlib +import json +import warnings +from datetime import datetime, timedelta +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context +from croniter import croniter + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +class ConfigError(Exception): + pass + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def spread_minute(name: str) -> int: + return int(hashlib.sha256(name.encode()).hexdigest(), 16) % 60 + + +def validated(cfg, registry: dict) -> dict: + if not isinstance(cfg, dict): + raise ConfigError("config is empty or not a mapping") + for key in ("name", "team", "schedule", "steps"): + if key not in cfg: + raise ConfigError(f"missing {key!r}") + if cfg["team"] not in registry["teams"]: + raise ConfigError(f"unknown team {cfg['team']!r}") + names = [s["name"] for s in cfg["steps"]] + if len(set(names)) != len(names): + raise ConfigError("duplicate step names") + deps = {s["name"]: list(s.get("depends_on", [])) for s in cfg["steps"]} + done: set[str] = set() + + def visit(node: str, path: tuple[str, ...]) -> None: + if node in path: + raise ConfigError(f"dependency cycle through {node!r}") + if node in done: + return + for up in deps.get(node, []): + visit(up, path + (node,)) + done.add(node) + + for n in names: + visit(n, ()) + for step in cfg["steps"]: + if step["table"] not in registry["datasets"]: + raise ConfigError(f"table {step['table']!r} is not registered") + for up in step.get("depends_on", []): + if up not in names: + raise ConfigError(f"step {step['name']!r} depends on unknown step {up!r}") + sched = cfg["schedule"] + minute = sched.get("minute", spread_minute(cfg["name"])) + cron = f"{minute} {sched['hour']} * * {sched.get('days', '*')}" + if not croniter.is_valid(cron): + raise ConfigError(f"invalid schedule {cron!r}") + return {**cfg, "cron": cron} + + +def build(cfg: dict, registry: dict, filename: str) -> DAG: + team = registry["teams"][cfg["team"]] + owners = sorted(set(team["owners"]) | set(cfg.get("owners", []))) + with DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=cfg["cron"], + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + doc_md=f"Generated from `{filename}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + "pool": "warehouse", + }, + ) as dag: + tasks = { + s["name"]: PythonOperator( + task_id=s["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": s["name"], "table": s["table"]}, + priority_weight=1000 if cfg.get("tier") == "critical" else 0, + weight_rule="absolute", + ) + for s in cfg["steps"] + } + for s in cfg["steps"]: + for up in s.get("depends_on", []): + tasks[up] >> tasks[s["name"]] + return dag + + +def main() -> None: + wanted = get_parsing_context().dag_id + paths = sorted(CONFIG_DIR.glob("*.yaml")) + if wanted and (CONFIG_DIR / f"{wanted}.yaml").is_file(): + paths = [CONFIG_DIR / f"{wanted}.yaml"] + registry = None + for path in paths: + try: + raw = yaml.safe_load(path.read_text()) + if wanted and isinstance(raw, dict) and raw.get("name") != wanted: + continue + registry = registry or yaml.safe_load(REGISTRY.read_text()) + build(validated(raw, registry), registry, path.name) + except Exception as exc: # one bad config must not hide the others + warnings.warn(f"pipeline config {path.name} skipped: {exc}", stacklevel=1) + + +main() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/pools.json b/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/pools.json new file mode 100644 index 0000000..dc37013 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/alt_reference/pools.json @@ -0,0 +1,4 @@ +{ + "default_pool": {"slots": 128, "description": "Default pool", "include_deferred": false}, + "warehouse": {"slots": 16, "description": "Warehouse query slots shared by the whole fleet", "include_deferred": false} +} diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/case.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/case.yaml new file mode 100644 index 0000000..35e1e1b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/case.yaml @@ -0,0 +1,104 @@ +id: hard-reliability-factory-fleet +area: debugging +suite: hard +split: dev +airflow_version: "3.3" +max_turns: 80 +timeout_s: 3600 +grade_timeout_s: 1500 +prompt: | + Our YAML pipeline fleet (dags/pipeline_factory.py, 140 configs under + configs/pipelines/) has become unreliable. From the last day of logs: + + dag-processor: + [2026-09-29T02:14:07.551+0000] {python_importer.py:244} ERROR - Failed to import: /opt/airflow/dags/pipeline_factory.py + airflow.sdk.exceptions.AirflowTaskTimeout: DagBag import timeout for /opt/airflow/dags/pipeline_factory.py after 30.0s. + Please take a look at these docs to improve your DAG import time: + * https://airflow.apache.org/docs/apache-airflow/3.3.2/best-practices.html#top-level-python-code + + dag-processor, Tuesday after the billing_reconcile merge: + [2026-09-29T16:02:51.090+0000] {dagbag.py:319} ERROR - Error loading DAG from /opt/airflow/dags/pipeline_factory.py: while scanning a simple key + in "/opt/airflow/configs/pipelines/billing_reconcile.yaml", line 9, column 1 + -> every fleet DAG disappeared from the UI until the config was reverted + + worker (every task, every pipeline): + [2026-09-29T03:41:12.004+0000] {task_runner.py:...} INFO - Parsing DAG file dags/pipeline_factory.py (28.7s) + + On top of that, the UI shows a new DAG version for every fleet DAG after + almost every parse. And at month-end the finance close (tier: critical + pipelines) ran three hours late: the warehouse only takes 16 concurrent + queries for the whole fleet, every step is a warehouse query, and the + standard pipelines kept it full. + + Please fix the factory so that: + - dags/pipeline_factory.py parses in under 5 seconds on a laptop (prod is + much slower and the import timeout is 30 s); + - every parse produces exactly the same DAGs, so there are no new versions + unless a config changes; adding, removing or breaking one config must not + change any other pipeline's DAG; + - a broken config only takes out its own DAG: that pipeline does not show + up at all until its config is fixed, every other pipeline keeps loading, + and the parse output logs an error that names the config file; + - when a worker parses the file to run a task, it builds only the DAG that + task belongs to, not all 140; + - the fleet never runs more than 16 warehouse queries at once: use a pool + named `warehouse` with 16 slots, defined in pools.json (the deploy runs + `airflow pools import pools.json`); + - when the warehouse is saturated, tasks of critical pipelines are always + picked before tasks of standard pipelines, however many steps either + pipeline has (teams keep adding steps); + - pipelines keep everything they have today: same dag_ids, tasks, + dependencies, owners, retries, hour and days. Start minutes must stay + spread over the hour (we can't have everything at :00) and an explicit + `schedule.minute` in a config wins. + + Don't edit configs/registry.yaml or the pipeline configs; the platform team + owns them. There's no scheduler running locally, only the airflow CLI. +description: | + Hard reliability case (Airflow 3.3): a YAML DAG factory at fleet scale. + The fixture re-reads a 0.5 MB registry with the pure-Python YAML loader once + per pipeline (over a minute, so the DagBag import timeout kills the file), + lets any bad config raise out of the module (all 140 DAGs vanish), injects + per-process values into every DAG (`hash(name) % 60` start minutes, + `",".join(set(owners))`, a parse timestamp in `doc_md`), builds all 140 + DAGs on every worker parse, and has no warehouse pool or tier priority. + What makes it hard: Airflow 3 runtime semantics around isolation. A + try/except around building each DAG is not enough: a DAG that raises inside + its `with DAG(...)` block is still auto-registered half-built (partial DAG in + the UI), and an invalid cron expression or a dependency cycle is only + detected when the DagBag validates the bag (`AirflowTimetableInvalid`, + cycle check), which becomes an import error for the whole factory file that + does not name the config. The fix has to validate per config (or build with + `auto_register=False` and call `dag.validate()`/`check_cycle()`), plus three + independent sources of serialization churn, the worker parsing context + (`get_parsing_context()`), a parse budget that rules out re-loading the + registry per pipeline even with libyaml, and absolute priority weights (the + default `downstream` weight rule lets a long standard pipeline outrank a + critical one). + Primary checks: import clean and all 140 DAGs present; tasks, deps, owners + (as a set) and retries per config; cron fire times for 8 runs match the + config's hour/days and explicit minutes; >= 30 distinct spread minutes and at + most 8 pipelines per minute; three parses (PYTHONHASHSEED 1/2/3, three time + zones, the last with the clock 3 days 7 hours ahead) give identical dag_hash + for every DAG; parse <= 5 s (median of three, DagBag stats); eight broken + configs injected (bad YAML, empty file, unknown team, unknown dependency, + invalid day-of-week, dependency cycle, duplicate step, unregistered table): + all 140 DAGs still load with unchanged hashes, no broken DAG is loaded + (not even half-built), every broken file named in the parse output (logs or + captured warnings); adding a config and removing another changes no other + DAG; with `_AIRFLOW_PARSING_CONTEXT_DAG_ID` set, only that DAG is built and + it hashes the same as in a full parse; `airflow pools import pools.json` + gives pool `warehouse` with 16 slots; every task uses it; with a 400-step + standard pipeline and a 1-step critical pipeline added, every critical task's + scheduler priority weight (serialized task's weight rule) exceeds every + standard task's; `airflow dags test` of two pipelines runs every step once in + dependency order. + Mutants: `python_hash_minute`, `set_owner`, `now_doc_md`, `index_minute` + (minute from the config's index in the sorted file list), + `registry_per_pipeline_libyaml`, `no_validate_autoregister` (try/except per + config but default auto-registration and no `validate()`), `silent_skip`, + `no_parsing_context`, `downstream_weight_rule`, `dag_concurrency_not_pool` + (`max_active_tasks=16` per DAG instead of the pool); reviewer-added + adversarial mutants `minute_zero` (spread logic deleted), `outer_try` (one + try/except around the whole loop) and `pool_not_declared` (tasks use the + pool, pools.json unchanged). diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/README.md b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/README.md new file mode 100644 index 0000000..b3a5332 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/README.md @@ -0,0 +1,20 @@ +# pipeline-fleet + +Airflow 3.3 deployment that turns YAML pipeline configs into DAGs. + +- `dags/pipeline_factory.py`: one DAG per `configs/pipelines/*.yaml`. Each step + becomes a task; `depends_on` lists the steps it waits for. +- `configs/registry.yaml`: team and dataset registry exported by the platform + team on every deploy (owners, on-call, retry defaults, registered tables). + Do not edit it by hand. +- `pools.json`: Airflow pools; the deploy runs `airflow pools import pools.json`. +- `output/`: where the stand-in `run_step` records which steps ran. + +Pipeline config fields: `name` (the dag_id), `team`, `tier` (`critical` or +`standard`), `description`, +`schedule.hour`, `schedule.days` (cron day-of-week, default `*`), optional +`schedule.minute` (otherwise the factory picks one to spread the load), +optional `owners` (added to the team's owners), optional `retries` (otherwise +the team default) and `steps` (`name`, `table`, `depends_on`). + +Local run: `airflow dags test billing_carriers 2026-03-02`. diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_attribution.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_attribution.yaml new file mode 100644 index 0000000..2c66071 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_attribution.yaml @@ -0,0 +1,32 @@ +name: billing_attribution +team: billing +tier: standard +description: Attribution pipeline for billing +schedule: + hour: 4 + days: '1' +retries: 0 +steps: +- name: extract + table: billing.raw_attribution_00 +- name: stage + table: billing.stg_attribution_00 + depends_on: + - extract +- name: validate + table: billing.int_attribution_00 + depends_on: + - stage + - extract +- name: transform + table: billing.int_attribution_00 + depends_on: + - validate +- name: aggregate + table: billing.int_attribution_00 + depends_on: + - transform +- name: publish + table: billing.mart_attribution_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_carriers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_carriers.yaml new file mode 100644 index 0000000..1199ca6 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_carriers.yaml @@ -0,0 +1,18 @@ +name: billing_carriers +team: billing +tier: standard +description: Carriers pipeline for billing +schedule: + hour: 1 + days: '*' +steps: +- name: extract + table: billing.raw_carriers_00 +- name: snapshot + table: billing.int_carriers_00 + depends_on: + - extract +- name: publish + table: billing.mart_carriers_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_chargebacks.yaml new file mode 100644 index 0000000..0f304f6 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_chargebacks.yaml @@ -0,0 +1,26 @@ +name: billing_chargebacks +team: billing +tier: critical +description: Chargebacks pipeline for billing +schedule: + hour: 3 + days: '*' +steps: +- name: extract + table: billing.raw_chargebacks_00 +- name: stage + table: billing.stg_chargebacks_00 + depends_on: + - extract +- name: validate + table: billing.int_chargebacks_00 + depends_on: + - stage +- name: enrich + table: billing.int_chargebacks_00 + depends_on: + - validate +- name: publish + table: billing.mart_chargebacks_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_forecast.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_forecast.yaml new file mode 100644 index 0000000..ea4086e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_forecast.yaml @@ -0,0 +1,31 @@ +name: billing_forecast +team: billing +tier: standard +description: Forecast pipeline for billing +schedule: + hour: 2 + days: 1-5 +steps: +- name: extract + table: billing.raw_forecast_00 +- name: validate + table: billing.int_forecast_00 + depends_on: + - extract +- name: transform + table: billing.int_forecast_00 + depends_on: + - validate +- name: enrich + table: billing.int_forecast_00 + depends_on: + - transform +- name: aggregate + table: billing.int_forecast_00 + depends_on: + - enrich +- name: publish + table: billing.mart_forecast_00 + depends_on: + - aggregate + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_fraud_scores.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_fraud_scores.yaml new file mode 100644 index 0000000..e76ec9e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_fraud_scores.yaml @@ -0,0 +1,33 @@ +name: billing_fraud_scores +team: billing +tier: standard +description: Fraud Scores pipeline for billing +schedule: + hour: 1 + days: '1' + minute: 15 +steps: +- name: extract + table: billing.raw_fraud_scores_00 +- name: stage + table: billing.stg_fraud_scores_00 + depends_on: + - extract +- name: validate + table: billing.int_fraud_scores_00 + depends_on: + - stage + - extract +- name: transform + table: billing.int_fraud_scores_00 + depends_on: + - validate +- name: aggregate + table: billing.int_fraud_scores_00 + depends_on: + - transform +- name: publish + table: billing.mart_fraud_scores_00 + depends_on: + - aggregate + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_orders.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_orders.yaml new file mode 100644 index 0000000..0ee1231 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_orders.yaml @@ -0,0 +1,33 @@ +name: billing_orders +team: billing +tier: standard +description: Orders pipeline for billing +schedule: + hour: 1 + days: 1-5 +steps: +- name: extract + table: billing.raw_orders_00 +- name: transform + table: billing.int_orders_00 + depends_on: + - extract +- name: enrich + table: billing.int_orders_00 + depends_on: + - transform +- name: aggregate + table: billing.int_orders_00 + depends_on: + - enrich + - transform +- name: snapshot + table: billing.int_orders_00 + depends_on: + - aggregate + - enrich +- name: publish + table: billing.mart_orders_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_promotions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_promotions.yaml new file mode 100644 index 0000000..87543ab --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_promotions.yaml @@ -0,0 +1,35 @@ +name: billing_promotions +team: billing +tier: standard +description: Promotions pipeline for billing +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: billing.raw_promotions_00 +- name: stage + table: billing.stg_promotions_00 + depends_on: + - extract +- name: transform + table: billing.int_promotions_00 + depends_on: + - stage +- name: enrich + table: billing.int_promotions_00 + depends_on: + - transform +- name: aggregate + table: billing.int_promotions_00 + depends_on: + - enrich +- name: snapshot + table: billing.int_promotions_00 + depends_on: + - aggregate +- name: publish + table: billing.mart_promotions_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_sessions.yaml new file mode 100644 index 0000000..c1f2c9a --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_sessions.yaml @@ -0,0 +1,24 @@ +name: billing_sessions +team: billing +tier: standard +description: Sessions pipeline for billing +schedule: + hour: 4 + days: '1' + minute: 45 +retries: 1 +steps: +- name: extract + table: billing.raw_sessions_00 +- name: enrich + table: billing.int_sessions_00 + depends_on: + - extract +- name: aggregate + table: billing.int_sessions_00 + depends_on: + - enrich +- name: publish + table: billing.mart_sessions_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend.yaml new file mode 100644 index 0000000..86fbb97 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend.yaml @@ -0,0 +1,28 @@ +name: billing_spend +team: billing +tier: standard +description: Spend pipeline for billing +schedule: + hour: 2 + days: '*' +owners: +- ana +steps: +- name: extract + table: billing.raw_spend_00 +- name: stage + table: billing.stg_spend_00 + depends_on: + - extract +- name: aggregate + table: billing.int_spend_00 + depends_on: + - stage +- name: snapshot + table: billing.int_spend_00 + depends_on: + - aggregate +- name: publish + table: billing.mart_spend_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend_eu.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend_eu.yaml new file mode 100644 index 0000000..79a1f5c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_spend_eu.yaml @@ -0,0 +1,27 @@ +name: billing_spend_eu +team: billing +tier: standard +description: Spend pipeline for billing +schedule: + hour: 4 + days: '*' + minute: 0 +steps: +- name: extract + table: billing.raw_spend_00 +- name: enrich + table: billing.int_spend_00 + depends_on: + - extract +- name: aggregate + table: billing.int_spend_00 + depends_on: + - enrich +- name: snapshot + table: billing.int_spend_00 + depends_on: + - aggregate +- name: publish + table: billing.mart_spend_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_tickets.yaml new file mode 100644 index 0000000..8227546 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_tickets.yaml @@ -0,0 +1,26 @@ +name: billing_tickets +team: billing +tier: standard +description: Tickets pipeline for billing +schedule: + hour: 2 + days: '*' +owners: +- hana +- quinn +retries: 4 +steps: +- name: extract + table: billing.raw_tickets_00 +- name: transform + table: billing.int_tickets_00 + depends_on: + - extract +- name: enrich + table: billing.int_tickets_00 + depends_on: + - transform +- name: publish + table: billing.mart_tickets_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_warehouses.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_warehouses.yaml new file mode 100644 index 0000000..0ec6181 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/billing_warehouses.yaml @@ -0,0 +1,27 @@ +name: billing_warehouses +team: billing +tier: standard +description: Warehouses pipeline for billing +schedule: + hour: 2 + days: 1-5 +retries: 1 +steps: +- name: extract + table: billing.raw_warehouses_00 +- name: enrich + table: billing.int_warehouses_00 + depends_on: + - extract +- name: aggregate + table: billing.int_warehouses_00 + depends_on: + - enrich +- name: snapshot + table: billing.int_warehouses_00 + depends_on: + - aggregate +- name: publish + table: billing.mart_warehouses_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_campaigns.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_campaigns.yaml new file mode 100644 index 0000000..7a83b59 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_campaigns.yaml @@ -0,0 +1,26 @@ +name: catalog_campaigns +team: catalog +tier: standard +description: Campaigns pipeline for catalog +schedule: + hour: 5 + days: '*' +owners: +- ben +- dara +steps: +- name: extract + table: catalog.raw_campaigns_00 +- name: validate + table: catalog.int_campaigns_00 + depends_on: + - extract +- name: enrich + table: catalog.int_campaigns_00 + depends_on: + - validate + - extract +- name: publish + table: catalog.mart_campaigns_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_chargebacks.yaml new file mode 100644 index 0000000..f6df0ad --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_chargebacks.yaml @@ -0,0 +1,33 @@ +name: catalog_chargebacks +team: catalog +tier: standard +description: Chargebacks pipeline for catalog +schedule: + hour: 0 + days: '*' +owners: +- dara +- yusuf +steps: +- name: extract + table: catalog.raw_chargebacks_00 +- name: stage + table: catalog.stg_chargebacks_00 + depends_on: + - extract +- name: transform + table: catalog.int_chargebacks_00 + depends_on: + - stage +- name: enrich + table: catalog.int_chargebacks_00 + depends_on: + - transform +- name: snapshot + table: catalog.int_chargebacks_00 + depends_on: + - enrich +- name: publish + table: catalog.mart_chargebacks_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_cohorts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_cohorts.yaml new file mode 100644 index 0000000..44ab4d3 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_cohorts.yaml @@ -0,0 +1,30 @@ +name: catalog_cohorts +team: catalog +tier: critical +description: Cohorts pipeline for catalog +schedule: + hour: 6 + days: '*' +steps: +- name: extract + table: catalog.raw_cohorts_00 +- name: stage + table: catalog.stg_cohorts_00 + depends_on: + - extract +- name: enrich + table: catalog.int_cohorts_00 + depends_on: + - stage +- name: aggregate + table: catalog.int_cohorts_00 + depends_on: + - enrich +- name: snapshot + table: catalog.int_cohorts_00 + depends_on: + - aggregate +- name: publish + table: catalog.mart_cohorts_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_forecast.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_forecast.yaml new file mode 100644 index 0000000..850cfe5 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_forecast.yaml @@ -0,0 +1,23 @@ +name: catalog_forecast +team: catalog +tier: standard +description: Forecast pipeline for catalog +schedule: + hour: 2 + days: '*' + minute: 15 +steps: +- name: extract + table: catalog.raw_forecast_00 +- name: stage + table: catalog.stg_forecast_00 + depends_on: + - extract +- name: transform + table: catalog.int_forecast_00 + depends_on: + - stage +- name: publish + table: catalog.mart_forecast_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders.yaml new file mode 100644 index 0000000..e8ceb61 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders.yaml @@ -0,0 +1,31 @@ +name: catalog_orders +team: catalog +tier: standard +description: Orders pipeline for catalog +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: catalog.raw_orders_00 +- name: validate + table: catalog.int_orders_00 + depends_on: + - extract +- name: transform + table: catalog.int_orders_00 + depends_on: + - validate +- name: enrich + table: catalog.int_orders_00 + depends_on: + - transform + - validate +- name: aggregate + table: catalog.int_orders_00 + depends_on: + - enrich +- name: publish + table: catalog.mart_orders_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders_backfill_safe.yaml new file mode 100644 index 0000000..a979a81 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_orders_backfill_safe.yaml @@ -0,0 +1,24 @@ +name: catalog_orders_backfill_safe +team: catalog +tier: standard +description: Orders pipeline for catalog +schedule: + hour: 4 + days: 1-5 + minute: 15 +retries: 4 +steps: +- name: extract + table: catalog.raw_orders_00 +- name: transform + table: catalog.int_orders_00 + depends_on: + - extract +- name: enrich + table: catalog.int_orders_00 + depends_on: + - transform +- name: publish + table: catalog.mart_orders_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_prices.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_prices.yaml new file mode 100644 index 0000000..81c4d42 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_prices.yaml @@ -0,0 +1,38 @@ +name: catalog_prices +team: catalog +tier: standard +description: Prices pipeline for catalog +schedule: + hour: 3 + days: '*' +owners: +- chen +- nils +steps: +- name: extract + table: catalog.raw_prices_00 +- name: stage + table: catalog.stg_prices_00 + depends_on: + - extract +- name: validate + table: catalog.int_prices_00 + depends_on: + - stage +- name: transform + table: catalog.int_prices_00 + depends_on: + - validate + - stage +- name: aggregate + table: catalog.int_prices_00 + depends_on: + - transform +- name: snapshot + table: catalog.int_prices_00 + depends_on: + - aggregate +- name: publish + table: catalog.mart_prices_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_products.yaml new file mode 100644 index 0000000..7d29404 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_products.yaml @@ -0,0 +1,21 @@ +name: catalog_products +team: catalog +tier: standard +description: Products pipeline for catalog +schedule: + hour: 1 + days: '*' +owners: +- yusuf +steps: +- name: extract + table: catalog.raw_products_00 +- name: enrich + table: catalog.int_products_00 + depends_on: + - extract +- name: publish + table: catalog.mart_products_00 + depends_on: + - enrich + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_promotions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_promotions.yaml new file mode 100644 index 0000000..8824ca7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_promotions.yaml @@ -0,0 +1,19 @@ +name: catalog_promotions +team: catalog +tier: standard +description: Promotions pipeline for catalog +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: catalog.raw_promotions_00 +- name: enrich + table: catalog.int_promotions_00 + depends_on: + - extract +- name: publish + table: catalog.mart_promotions_00 + depends_on: + - enrich + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_sessions.yaml new file mode 100644 index 0000000..2f2c48d --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_sessions.yaml @@ -0,0 +1,18 @@ +name: catalog_sessions +team: catalog +tier: standard +description: Sessions pipeline for catalog +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: catalog.raw_sessions_00 +- name: validate + table: catalog.int_sessions_00 + depends_on: + - extract +- name: publish + table: catalog.mart_sessions_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_signups.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_signups.yaml new file mode 100644 index 0000000..2b79c2f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_signups.yaml @@ -0,0 +1,24 @@ +name: catalog_signups +team: catalog +tier: critical +description: Signups pipeline for catalog +schedule: + hour: 3 + days: '*' +retries: 0 +steps: +- name: extract + table: catalog.raw_signups_00 +- name: stage + table: catalog.stg_signups_00 + depends_on: + - extract +- name: snapshot + table: catalog.int_signups_00 + depends_on: + - stage + - extract +- name: publish + table: catalog.mart_signups_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_tickets.yaml new file mode 100644 index 0000000..dbb7fe9 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/catalog_tickets.yaml @@ -0,0 +1,26 @@ +name: catalog_tickets +team: catalog +tier: standard +description: Tickets pipeline for catalog +schedule: + hour: 5 + days: '1' +owners: +- yusuf +- dara +steps: +- name: extract + table: catalog.raw_tickets_00 +- name: stage + table: catalog.stg_tickets_00 + depends_on: + - extract +- name: validate + table: catalog.int_tickets_00 + depends_on: + - stage + - extract +- name: publish + table: catalog.mart_tickets_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_attribution.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_attribution.yaml new file mode 100644 index 0000000..7e071ed --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_attribution.yaml @@ -0,0 +1,23 @@ +name: finance_attribution +team: finance +tier: standard +description: Attribution pipeline for finance +schedule: + hour: 5 + days: '1' +steps: +- name: extract + table: finance.raw_attribution_00 +- name: stage + table: finance.stg_attribution_00 + depends_on: + - extract +- name: aggregate + table: finance.int_attribution_00 + depends_on: + - stage + - extract +- name: publish + table: finance.mart_attribution_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_chargebacks.yaml new file mode 100644 index 0000000..0ddcece --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_chargebacks.yaml @@ -0,0 +1,32 @@ +name: finance_chargebacks +team: finance +tier: standard +description: Chargebacks pipeline for finance +schedule: + hour: 4 + days: 1-5 +steps: +- name: extract + table: finance.raw_chargebacks_00 +- name: stage + table: finance.stg_chargebacks_00 + depends_on: + - extract +- name: validate + table: finance.int_chargebacks_00 + depends_on: + - stage +- name: enrich + table: finance.int_chargebacks_00 + depends_on: + - validate + - stage +- name: aggregate + table: finance.int_chargebacks_00 + depends_on: + - enrich +- name: publish + table: finance.mart_chargebacks_00 + depends_on: + - aggregate + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_clicks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_clicks.yaml new file mode 100644 index 0000000..b283d9c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_clicks.yaml @@ -0,0 +1,18 @@ +name: finance_clicks +team: finance +tier: standard +description: Clicks pipeline for finance +schedule: + hour: 5 + days: '1' +steps: +- name: extract + table: finance.raw_clicks_00 +- name: enrich + table: finance.int_clicks_00 + depends_on: + - extract +- name: publish + table: finance.mart_clicks_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_fraud_scores.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_fraud_scores.yaml new file mode 100644 index 0000000..eeac1bd --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_fraud_scores.yaml @@ -0,0 +1,18 @@ +name: finance_fraud_scores +team: finance +tier: standard +description: Fraud Scores pipeline for finance +schedule: + hour: 6 + days: '*' +steps: +- name: extract + table: finance.raw_fraud_scores_00 +- name: validate + table: finance.int_fraud_scores_00 + depends_on: + - extract +- name: publish + table: finance.mart_fraud_scores_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory.yaml new file mode 100644 index 0000000..30bd707 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory.yaml @@ -0,0 +1,19 @@ +name: finance_inventory +team: finance +tier: standard +description: Inventory pipeline for finance +schedule: + hour: 5 + days: '*' + minute: 45 +steps: +- name: extract + table: finance.raw_inventory_00 +- name: enrich + table: finance.int_inventory_00 + depends_on: + - extract +- name: publish + table: finance.mart_inventory_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory_us.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory_us.yaml new file mode 100644 index 0000000..11b9175 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_inventory_us.yaml @@ -0,0 +1,23 @@ +name: finance_inventory_us +team: finance +tier: standard +description: Inventory pipeline for finance +schedule: + hour: 2 + days: 1-5 + minute: 0 +steps: +- name: extract + table: finance.raw_inventory_00 +- name: stage + table: finance.stg_inventory_00 + depends_on: + - extract +- name: snapshot + table: finance.int_inventory_00 + depends_on: + - stage +- name: publish + table: finance.mart_inventory_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_ledger.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_ledger.yaml new file mode 100644 index 0000000..e8ab0eb --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_ledger.yaml @@ -0,0 +1,24 @@ +name: finance_ledger +team: finance +tier: critical +description: Ledger pipeline for finance +schedule: + hour: 4 + days: 1-5 +owners: +- eli +steps: +- name: extract + table: finance.raw_ledger_00 +- name: transform + table: finance.int_ledger_00 + depends_on: + - extract +- name: snapshot + table: finance.int_ledger_00 + depends_on: + - transform +- name: publish + table: finance.mart_ledger_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_payouts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_payouts.yaml new file mode 100644 index 0000000..7b97920 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_payouts.yaml @@ -0,0 +1,30 @@ +name: finance_payouts +team: finance +tier: critical +description: Payouts pipeline for finance +schedule: + hour: 4 + days: '*' +owners: +- pia +retries: 1 +steps: +- name: extract + table: finance.raw_payouts_00 +- name: transform + table: finance.int_payouts_00 + depends_on: + - extract +- name: aggregate + table: finance.int_payouts_00 + depends_on: + - transform + - extract +- name: snapshot + table: finance.int_payouts_00 + depends_on: + - aggregate +- name: publish + table: finance.mart_payouts_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_promotions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_promotions.yaml new file mode 100644 index 0000000..588243a --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_promotions.yaml @@ -0,0 +1,20 @@ +name: finance_promotions +team: finance +tier: standard +description: Promotions pipeline for finance +schedule: + hour: 0 + days: '*' +retries: 4 +steps: +- name: extract + table: finance.raw_promotions_00 +- name: snapshot + table: finance.int_promotions_00 + depends_on: + - extract +- name: publish + table: finance.mart_promotions_00 + depends_on: + - snapshot + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_sessions.yaml new file mode 100644 index 0000000..7141c66 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_sessions.yaml @@ -0,0 +1,29 @@ +name: finance_sessions +team: finance +tier: standard +description: Sessions pipeline for finance +schedule: + hour: 6 + days: 1-5 +owners: +- maya +- ana +steps: +- name: extract + table: finance.raw_sessions_00 +- name: stage + table: finance.stg_sessions_00 + depends_on: + - extract +- name: validate + table: finance.int_sessions_00 + depends_on: + - stage +- name: enrich + table: finance.int_sessions_00 + depends_on: + - validate +- name: publish + table: finance.mart_sessions_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets.yaml new file mode 100644 index 0000000..955f365 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets.yaml @@ -0,0 +1,39 @@ +name: finance_tickets +team: finance +tier: critical +description: Tickets pipeline for finance +schedule: + hour: 2 + days: '*' + minute: 30 +owners: +- vik +retries: 4 +steps: +- name: extract + table: finance.raw_tickets_00 +- name: stage + table: finance.stg_tickets_00 + depends_on: + - extract +- name: validate + table: finance.int_tickets_00 + depends_on: + - stage +- name: transform + table: finance.int_tickets_00 + depends_on: + - validate +- name: enrich + table: finance.int_tickets_00 + depends_on: + - transform + - validate +- name: snapshot + table: finance.int_tickets_00 + depends_on: + - enrich +- name: publish + table: finance.mart_tickets_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets_backfill_safe.yaml new file mode 100644 index 0000000..2e83262 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/finance_tickets_backfill_safe.yaml @@ -0,0 +1,19 @@ +name: finance_tickets_backfill_safe +team: finance +tier: standard +description: Tickets pipeline for finance +schedule: + hour: 2 + days: '*' +retries: 4 +steps: +- name: extract + table: finance.raw_tickets_00 +- name: snapshot + table: finance.int_tickets_00 + depends_on: + - extract +- name: publish + table: finance.mart_tickets_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_ledger.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_ledger.yaml new file mode 100644 index 0000000..c53f3ac --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_ledger.yaml @@ -0,0 +1,18 @@ +name: growth_ledger +team: growth +tier: standard +description: Ledger pipeline for growth +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: growth.raw_ledger_00 +- name: snapshot + table: growth.int_ledger_00 + depends_on: + - extract +- name: publish + table: growth.mart_ledger_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders.yaml new file mode 100644 index 0000000..e036bab --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders.yaml @@ -0,0 +1,33 @@ +name: growth_orders +team: growth +tier: standard +description: Orders pipeline for growth +schedule: + hour: 3 + days: 1-5 +retries: 4 +steps: +- name: extract + table: growth.raw_orders_00 +- name: stage + table: growth.stg_orders_00 + depends_on: + - extract +- name: validate + table: growth.int_orders_00 + depends_on: + - stage + - extract +- name: enrich + table: growth.int_orders_00 + depends_on: + - validate +- name: aggregate + table: growth.int_orders_00 + depends_on: + - enrich + - validate +- name: publish + table: growth.mart_orders_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_hourly_rollup.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_hourly_rollup.yaml new file mode 100644 index 0000000..0001382 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_hourly_rollup.yaml @@ -0,0 +1,20 @@ +name: growth_orders_hourly_rollup +team: growth +tier: standard +description: Orders pipeline for growth +schedule: + hour: 3 + days: '*' +retries: 4 +steps: +- name: extract + table: growth.raw_orders_00 +- name: stage + table: growth.stg_orders_00 + depends_on: + - extract +- name: publish + table: growth.mart_orders_00 + depends_on: + - stage + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_v2.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_v2.yaml new file mode 100644 index 0000000..acd121c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_orders_v2.yaml @@ -0,0 +1,32 @@ +name: growth_orders_v2 +team: growth +tier: standard +description: Orders pipeline for growth +schedule: + hour: 0 + days: 1-5 +owners: +- quinn +steps: +- name: extract + table: growth.raw_orders_00 +- name: transform + table: growth.int_orders_00 + depends_on: + - extract +- name: enrich + table: growth.int_orders_00 + depends_on: + - transform +- name: aggregate + table: growth.int_orders_00 + depends_on: + - enrich +- name: snapshot + table: growth.int_orders_00 + depends_on: + - aggregate +- name: publish + table: growth.mart_orders_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_payouts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_payouts.yaml new file mode 100644 index 0000000..f256959 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_payouts.yaml @@ -0,0 +1,23 @@ +name: growth_payouts +team: growth +tier: critical +description: Payouts pipeline for growth +schedule: + hour: 3 + days: 1-5 + minute: 30 +steps: +- name: extract + table: growth.raw_payouts_00 +- name: stage + table: growth.stg_payouts_00 + depends_on: + - extract +- name: validate + table: growth.int_payouts_00 + depends_on: + - stage +- name: publish + table: growth.mart_payouts_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_prices.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_prices.yaml new file mode 100644 index 0000000..c1710ee --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_prices.yaml @@ -0,0 +1,36 @@ +name: growth_prices +team: growth +tier: standard +description: Prices pipeline for growth +schedule: + hour: 6 + days: '*' + minute: 45 +steps: +- name: extract + table: growth.raw_prices_00 +- name: stage + table: growth.stg_prices_00 + depends_on: + - extract +- name: transform + table: growth.int_prices_00 + depends_on: + - stage +- name: enrich + table: growth.int_prices_00 + depends_on: + - transform +- name: aggregate + table: growth.int_prices_00 + depends_on: + - enrich + - transform +- name: snapshot + table: growth.int_prices_00 + depends_on: + - aggregate +- name: publish + table: growth.mart_prices_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_products.yaml new file mode 100644 index 0000000..e1d9c57 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_products.yaml @@ -0,0 +1,23 @@ +name: growth_products +team: growth +tier: standard +description: Products pipeline for growth +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: growth.raw_products_00 +- name: stage + table: growth.stg_products_00 + depends_on: + - extract +- name: enrich + table: growth.int_products_00 + depends_on: + - stage + - extract +- name: publish + table: growth.mart_products_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_queries.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_queries.yaml new file mode 100644 index 0000000..6a5564e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_queries.yaml @@ -0,0 +1,23 @@ +name: growth_queries +team: growth +tier: critical +description: Queries pipeline for growth +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: growth.raw_queries_00 +- name: stage + table: growth.stg_queries_00 + depends_on: + - extract +- name: snapshot + table: growth.int_queries_00 + depends_on: + - stage +- name: publish + table: growth.mart_queries_00 + depends_on: + - snapshot + - stage diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds.yaml new file mode 100644 index 0000000..579865f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds.yaml @@ -0,0 +1,31 @@ +name: growth_refunds +team: growth +tier: standard +description: Refunds pipeline for growth +schedule: + hour: 2 + days: '*' + minute: 15 +steps: +- name: extract + table: growth.raw_refunds_00 +- name: stage + table: growth.stg_refunds_00 + depends_on: + - extract +- name: transform + table: growth.int_refunds_00 + depends_on: + - stage +- name: aggregate + table: growth.int_refunds_00 + depends_on: + - transform +- name: snapshot + table: growth.int_refunds_00 + depends_on: + - aggregate +- name: publish + table: growth.mart_refunds_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_backfill_safe.yaml new file mode 100644 index 0000000..34f984c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_backfill_safe.yaml @@ -0,0 +1,35 @@ +name: growth_refunds_backfill_safe +team: growth +tier: standard +description: Refunds pipeline for growth +schedule: + hour: 5 + days: '*' +retries: 0 +steps: +- name: extract + table: growth.raw_refunds_00 +- name: validate + table: growth.int_refunds_00 + depends_on: + - extract +- name: transform + table: growth.int_refunds_00 + depends_on: + - validate +- name: enrich + table: growth.int_refunds_00 + depends_on: + - transform +- name: aggregate + table: growth.int_refunds_00 + depends_on: + - enrich +- name: snapshot + table: growth.int_refunds_00 + depends_on: + - aggregate +- name: publish + table: growth.mart_refunds_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_hourly_rollup.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_hourly_rollup.yaml new file mode 100644 index 0000000..6f5fce6 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_refunds_hourly_rollup.yaml @@ -0,0 +1,38 @@ +name: growth_refunds_hourly_rollup +team: growth +tier: standard +description: Refunds pipeline for growth +schedule: + hour: 0 + days: 1-5 +retries: 1 +steps: +- name: extract + table: growth.raw_refunds_00 +- name: stage + table: growth.stg_refunds_00 + depends_on: + - extract +- name: validate + table: growth.int_refunds_00 + depends_on: + - stage + - extract +- name: transform + table: growth.int_refunds_00 + depends_on: + - validate +- name: enrich + table: growth.int_refunds_00 + depends_on: + - transform + - validate +- name: snapshot + table: growth.int_refunds_00 + depends_on: + - enrich + - transform +- name: publish + table: growth.mart_refunds_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_spend.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_spend.yaml new file mode 100644 index 0000000..3de8371 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/growth_spend.yaml @@ -0,0 +1,28 @@ +name: growth_spend +team: growth +tier: standard +description: Spend pipeline for growth +schedule: + hour: 1 + days: 1-5 +steps: +- name: extract + table: growth.raw_spend_00 +- name: stage + table: growth.stg_spend_00 + depends_on: + - extract +- name: transform + table: growth.int_spend_00 + depends_on: + - stage + - extract +- name: snapshot + table: growth.int_spend_00 + depends_on: + - transform + - stage +- name: publish + table: growth.mart_spend_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_clicks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_clicks.yaml new file mode 100644 index 0000000..4d48eed --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_clicks.yaml @@ -0,0 +1,36 @@ +name: logistics_clicks +team: logistics +tier: standard +description: Clicks pipeline for logistics +schedule: + hour: 3 + days: 1-5 + minute: 30 +steps: +- name: extract + table: logistics.raw_clicks_00 +- name: stage + table: logistics.stg_clicks_00 + depends_on: + - extract +- name: validate + table: logistics.int_clicks_00 + depends_on: + - stage +- name: transform + table: logistics.int_clicks_00 + depends_on: + - validate + - stage +- name: enrich + table: logistics.int_clicks_00 + depends_on: + - transform +- name: aggregate + table: logistics.int_clicks_00 + depends_on: + - enrich +- name: publish + table: logistics.mart_clicks_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payouts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payouts.yaml new file mode 100644 index 0000000..7a41903 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payouts.yaml @@ -0,0 +1,25 @@ +name: logistics_payouts +team: logistics +tier: critical +description: Payouts pipeline for logistics +schedule: + hour: 1 + days: '1' +owners: +- lena +- fatima +steps: +- name: extract + table: logistics.raw_payouts_00 +- name: transform + table: logistics.int_payouts_00 + depends_on: + - extract +- name: aggregate + table: logistics.int_payouts_00 + depends_on: + - transform +- name: publish + table: logistics.mart_payouts_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payroll.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payroll.yaml new file mode 100644 index 0000000..b641ba7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_payroll.yaml @@ -0,0 +1,28 @@ +name: logistics_payroll +team: logistics +tier: standard +description: Payroll pipeline for logistics +schedule: + hour: 5 + days: 1-5 +retries: 4 +steps: +- name: extract + table: logistics.raw_payroll_00 +- name: transform + table: logistics.int_payroll_00 + depends_on: + - extract +- name: enrich + table: logistics.int_payroll_00 + depends_on: + - transform + - extract +- name: aggregate + table: logistics.int_payroll_00 + depends_on: + - enrich +- name: publish + table: logistics.mart_payroll_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_prices.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_prices.yaml new file mode 100644 index 0000000..68d37ee --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_prices.yaml @@ -0,0 +1,30 @@ +name: logistics_prices +team: logistics +tier: critical +description: Prices pipeline for logistics +schedule: + hour: 2 + days: '1' +owners: +- tara +- ana +steps: +- name: extract + table: logistics.raw_prices_00 +- name: stage + table: logistics.stg_prices_00 + depends_on: + - extract +- name: validate + table: logistics.int_prices_00 + depends_on: + - stage + - extract +- name: transform + table: logistics.int_prices_00 + depends_on: + - validate +- name: publish + table: logistics.mart_prices_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_promotions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_promotions.yaml new file mode 100644 index 0000000..8ed4fff --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_promotions.yaml @@ -0,0 +1,39 @@ +name: logistics_promotions +team: logistics +tier: standard +description: Promotions pipeline for logistics +schedule: + hour: 5 + days: '1' + minute: 15 +owners: +- tara +retries: 0 +steps: +- name: extract + table: logistics.raw_promotions_00 +- name: stage + table: logistics.stg_promotions_00 + depends_on: + - extract +- name: validate + table: logistics.int_promotions_00 + depends_on: + - stage +- name: transform + table: logistics.int_promotions_00 + depends_on: + - validate + - stage +- name: aggregate + table: logistics.int_promotions_00 + depends_on: + - transform +- name: snapshot + table: logistics.int_promotions_00 + depends_on: + - aggregate +- name: publish + table: logistics.mart_promotions_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_queries.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_queries.yaml new file mode 100644 index 0000000..edd04ba --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_queries.yaml @@ -0,0 +1,19 @@ +name: logistics_queries +team: logistics +tier: standard +description: Queries pipeline for logistics +schedule: + hour: 3 + days: 1-5 +retries: 4 +steps: +- name: extract + table: logistics.raw_queries_00 +- name: enrich + table: logistics.int_queries_00 + depends_on: + - extract +- name: publish + table: logistics.mart_queries_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_settlements.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_settlements.yaml new file mode 100644 index 0000000..52e0733 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_settlements.yaml @@ -0,0 +1,23 @@ +name: logistics_settlements +team: logistics +tier: standard +description: Settlements pipeline for logistics +schedule: + hour: 0 + days: '*' + minute: 30 +steps: +- name: extract + table: logistics.raw_settlements_00 +- name: aggregate + table: logistics.int_settlements_00 + depends_on: + - extract +- name: snapshot + table: logistics.int_settlements_00 + depends_on: + - aggregate +- name: publish + table: logistics.mart_settlements_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_shipments.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_shipments.yaml new file mode 100644 index 0000000..397a58f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_shipments.yaml @@ -0,0 +1,28 @@ +name: logistics_shipments +team: logistics +tier: standard +description: Shipments pipeline for logistics +schedule: + hour: 4 + days: '1' +retries: 0 +steps: +- name: extract + table: logistics.raw_shipments_00 +- name: validate + table: logistics.int_shipments_00 + depends_on: + - extract +- name: enrich + table: logistics.int_shipments_00 + depends_on: + - validate + - extract +- name: aggregate + table: logistics.int_shipments_00 + depends_on: + - enrich +- name: publish + table: logistics.mart_shipments_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups.yaml new file mode 100644 index 0000000..2597e63 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups.yaml @@ -0,0 +1,26 @@ +name: logistics_signups +team: logistics +tier: standard +description: Signups pipeline for logistics +schedule: + hour: 2 + days: '*' +owners: +- uma +steps: +- name: extract + table: logistics.raw_signups_00 +- name: stage + table: logistics.stg_signups_00 + depends_on: + - extract +- name: enrich + table: logistics.int_signups_00 + depends_on: + - stage + - extract +- name: publish + table: logistics.mart_signups_00 + depends_on: + - enrich + - stage diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups_daily.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups_daily.yaml new file mode 100644 index 0000000..25a31ea --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_signups_daily.yaml @@ -0,0 +1,19 @@ +name: logistics_signups_daily +team: logistics +tier: standard +description: Signups pipeline for logistics +schedule: + hour: 0 + days: '*' + minute: 30 +steps: +- name: extract + table: logistics.raw_signups_00 +- name: transform + table: logistics.int_signups_00 + depends_on: + - extract +- name: publish + table: logistics.mart_signups_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers.yaml new file mode 100644 index 0000000..c501fda --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers.yaml @@ -0,0 +1,26 @@ +name: logistics_suppliers +team: logistics +tier: standard +description: Suppliers pipeline for logistics +schedule: + hour: 4 + days: '1' +steps: +- name: extract + table: logistics.raw_suppliers_00 +- name: stage + table: logistics.stg_suppliers_00 + depends_on: + - extract +- name: validate + table: logistics.int_suppliers_00 + depends_on: + - stage +- name: enrich + table: logistics.int_suppliers_00 + depends_on: + - validate +- name: publish + table: logistics.mart_suppliers_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers_daily.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers_daily.yaml new file mode 100644 index 0000000..0c4c2a8 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/logistics_suppliers_daily.yaml @@ -0,0 +1,37 @@ +name: logistics_suppliers_daily +team: logistics +tier: standard +description: Suppliers pipeline for logistics +schedule: + hour: 5 + days: '*' +owners: +- dara +- omar +steps: +- name: extract + table: logistics.raw_suppliers_00 +- name: stage + table: logistics.stg_suppliers_00 + depends_on: + - extract +- name: validate + table: logistics.int_suppliers_00 + depends_on: + - stage +- name: enrich + table: logistics.int_suppliers_00 + depends_on: + - validate +- name: aggregate + table: logistics.int_suppliers_00 + depends_on: + - enrich +- name: snapshot + table: logistics.int_suppliers_00 + depends_on: + - aggregate +- name: publish + table: logistics.mart_suppliers_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_campaigns.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_campaigns.yaml new file mode 100644 index 0000000..f5fddb4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_campaigns.yaml @@ -0,0 +1,18 @@ +name: marketing_campaigns +team: marketing +tier: critical +description: Campaigns pipeline for marketing +schedule: + hour: 1 + days: '*' +steps: +- name: extract + table: marketing.raw_campaigns_00 +- name: validate + table: marketing.int_campaigns_00 + depends_on: + - extract +- name: publish + table: marketing.mart_campaigns_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_forecast.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_forecast.yaml new file mode 100644 index 0000000..283d565 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_forecast.yaml @@ -0,0 +1,37 @@ +name: marketing_forecast +team: marketing +tier: standard +description: Forecast pipeline for marketing +schedule: + hour: 1 + days: '*' +retries: 0 +steps: +- name: extract + table: marketing.raw_forecast_00 +- name: stage + table: marketing.stg_forecast_00 + depends_on: + - extract +- name: validate + table: marketing.int_forecast_00 + depends_on: + - stage +- name: transform + table: marketing.int_forecast_00 + depends_on: + - validate + - stage +- name: enrich + table: marketing.int_forecast_00 + depends_on: + - transform + - validate +- name: aggregate + table: marketing.int_forecast_00 + depends_on: + - enrich +- name: publish + table: marketing.mart_forecast_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_prices.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_prices.yaml new file mode 100644 index 0000000..66c82af --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_prices.yaml @@ -0,0 +1,33 @@ +name: marketing_prices +team: marketing +tier: critical +description: Prices pipeline for marketing +schedule: + hour: 2 + days: 1-5 + minute: 45 +retries: 1 +steps: +- name: extract + table: marketing.raw_prices_00 +- name: stage + table: marketing.stg_prices_00 + depends_on: + - extract +- name: validate + table: marketing.int_prices_00 + depends_on: + - stage + - extract +- name: enrich + table: marketing.int_prices_00 + depends_on: + - validate +- name: aggregate + table: marketing.int_prices_00 + depends_on: + - enrich +- name: publish + table: marketing.mart_prices_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries.yaml new file mode 100644 index 0000000..e39b863 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries.yaml @@ -0,0 +1,24 @@ +name: marketing_queries +team: marketing +tier: standard +description: Queries pipeline for marketing +schedule: + hour: 1 + days: '*' +owners: +- dara +steps: +- name: extract + table: marketing.raw_queries_00 +- name: transform + table: marketing.int_queries_00 + depends_on: + - extract +- name: aggregate + table: marketing.int_queries_00 + depends_on: + - transform +- name: publish + table: marketing.mart_queries_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries_v2.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries_v2.yaml new file mode 100644 index 0000000..933550b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_queries_v2.yaml @@ -0,0 +1,36 @@ +name: marketing_queries_v2 +team: marketing +tier: standard +description: Queries pipeline for marketing +schedule: + hour: 0 + days: '1' +steps: +- name: extract + table: marketing.raw_queries_00 +- name: stage + table: marketing.stg_queries_00 + depends_on: + - extract +- name: validate + table: marketing.int_queries_00 + depends_on: + - stage +- name: enrich + table: marketing.int_queries_00 + depends_on: + - validate +- name: aggregate + table: marketing.int_queries_00 + depends_on: + - enrich +- name: snapshot + table: marketing.int_queries_00 + depends_on: + - aggregate + - enrich +- name: publish + table: marketing.mart_queries_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_refunds.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_refunds.yaml new file mode 100644 index 0000000..04b361c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_refunds.yaml @@ -0,0 +1,18 @@ +name: marketing_refunds +team: marketing +tier: standard +description: Refunds pipeline for marketing +schedule: + hour: 3 + days: '1' +steps: +- name: extract + table: marketing.raw_refunds_00 +- name: aggregate + table: marketing.int_refunds_00 + depends_on: + - extract +- name: publish + table: marketing.mart_refunds_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions.yaml new file mode 100644 index 0000000..8c179e8 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions.yaml @@ -0,0 +1,22 @@ +name: marketing_sessions +team: marketing +tier: standard +description: Sessions pipeline for marketing +schedule: + hour: 3 + days: '*' +steps: +- name: extract + table: marketing.raw_sessions_00 +- name: stage + table: marketing.stg_sessions_00 + depends_on: + - extract +- name: transform + table: marketing.int_sessions_00 + depends_on: + - stage +- name: publish + table: marketing.mart_sessions_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions_daily.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions_daily.yaml new file mode 100644 index 0000000..1c2c137 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_sessions_daily.yaml @@ -0,0 +1,21 @@ +name: marketing_sessions_daily +team: marketing +tier: standard +description: Sessions pipeline for marketing +schedule: + hour: 1 + days: '1' + minute: 15 +retries: 0 +steps: +- name: extract + table: marketing.raw_sessions_00 +- name: enrich + table: marketing.int_sessions_00 + depends_on: + - extract +- name: publish + table: marketing.mart_sessions_00 + depends_on: + - enrich + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_settlements.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_settlements.yaml new file mode 100644 index 0000000..603bd59 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_settlements.yaml @@ -0,0 +1,22 @@ +name: marketing_settlements +team: marketing +tier: standard +description: Settlements pipeline for marketing +schedule: + hour: 3 + days: '*' +steps: +- name: extract + table: marketing.raw_settlements_00 +- name: enrich + table: marketing.int_settlements_00 + depends_on: + - extract +- name: aggregate + table: marketing.int_settlements_00 + depends_on: + - enrich +- name: publish + table: marketing.mart_settlements_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_suppliers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_suppliers.yaml new file mode 100644 index 0000000..28a93e3 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_suppliers.yaml @@ -0,0 +1,28 @@ +name: marketing_suppliers +team: marketing +tier: standard +description: Suppliers pipeline for marketing +schedule: + hour: 4 + days: '*' +retries: 4 +steps: +- name: extract + table: marketing.raw_suppliers_00 +- name: validate + table: marketing.int_suppliers_00 + depends_on: + - extract +- name: transform + table: marketing.int_suppliers_00 + depends_on: + - validate +- name: snapshot + table: marketing.int_suppliers_00 + depends_on: + - transform + - validate +- name: publish + table: marketing.mart_suppliers_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets.yaml new file mode 100644 index 0000000..c35a9b2 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets.yaml @@ -0,0 +1,31 @@ +name: marketing_tickets +team: marketing +tier: critical +description: Tickets pipeline for marketing +schedule: + hour: 4 + days: '*' +retries: 1 +steps: +- name: extract + table: marketing.raw_tickets_00 +- name: validate + table: marketing.int_tickets_00 + depends_on: + - extract +- name: transform + table: marketing.int_tickets_00 + depends_on: + - validate +- name: enrich + table: marketing.int_tickets_00 + depends_on: + - transform +- name: aggregate + table: marketing.int_tickets_00 + depends_on: + - enrich +- name: publish + table: marketing.mart_tickets_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets_hourly_rollup.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets_hourly_rollup.yaml new file mode 100644 index 0000000..24b1de7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/marketing_tickets_hourly_rollup.yaml @@ -0,0 +1,32 @@ +name: marketing_tickets_hourly_rollup +team: marketing +tier: standard +description: Tickets pipeline for marketing +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: marketing.raw_tickets_00 +- name: stage + table: marketing.stg_tickets_00 + depends_on: + - extract +- name: enrich + table: marketing.int_tickets_00 + depends_on: + - stage +- name: aggregate + table: marketing.int_tickets_00 + depends_on: + - enrich + - stage +- name: snapshot + table: marketing.int_tickets_00 + depends_on: + - aggregate + - enrich +- name: publish + table: marketing.mart_tickets_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_campaigns.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_campaigns.yaml new file mode 100644 index 0000000..a908110 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_campaigns.yaml @@ -0,0 +1,38 @@ +name: payments_campaigns +team: payments +tier: standard +description: Campaigns pipeline for payments +schedule: + hour: 1 + days: '*' +owners: +- ben +- maya +steps: +- name: extract + table: payments.raw_campaigns_00 +- name: stage + table: payments.stg_campaigns_00 + depends_on: + - extract +- name: validate + table: payments.int_campaigns_00 + depends_on: + - stage +- name: transform + table: payments.int_campaigns_00 + depends_on: + - validate +- name: enrich + table: payments.int_campaigns_00 + depends_on: + - transform +- name: snapshot + table: payments.int_campaigns_00 + depends_on: + - enrich + - transform +- name: publish + table: payments.mart_campaigns_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_carriers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_carriers.yaml new file mode 100644 index 0000000..8cbba83 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_carriers.yaml @@ -0,0 +1,18 @@ +name: payments_carriers +team: payments +tier: critical +description: Carriers pipeline for payments +schedule: + hour: 1 + days: '*' +steps: +- name: extract + table: payments.raw_carriers_00 +- name: transform + table: payments.int_carriers_00 + depends_on: + - extract +- name: publish + table: payments.mart_carriers_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat.yaml new file mode 100644 index 0000000..fdd3ebb --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat.yaml @@ -0,0 +1,37 @@ +name: payments_csat +team: payments +tier: standard +description: Csat pipeline for payments +schedule: + hour: 0 + days: '1' +retries: 1 +steps: +- name: extract + table: payments.raw_csat_00 +- name: stage + table: payments.stg_csat_00 + depends_on: + - extract +- name: validate + table: payments.int_csat_00 + depends_on: + - stage + - extract +- name: transform + table: payments.int_csat_00 + depends_on: + - validate +- name: enrich + table: payments.int_csat_00 + depends_on: + - transform +- name: snapshot + table: payments.int_csat_00 + depends_on: + - enrich + - transform +- name: publish + table: payments.mart_csat_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat_eu.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat_eu.yaml new file mode 100644 index 0000000..4009f2b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_csat_eu.yaml @@ -0,0 +1,19 @@ +name: payments_csat_eu +team: payments +tier: standard +description: Csat pipeline for payments +schedule: + hour: 6 + days: '1' +steps: +- name: extract + table: payments.raw_csat_00 +- name: validate + table: payments.int_csat_00 + depends_on: + - extract +- name: publish + table: payments.mart_csat_00 + depends_on: + - validate + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_headcount.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_headcount.yaml new file mode 100644 index 0000000..5a6516f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_headcount.yaml @@ -0,0 +1,33 @@ +name: payments_headcount +team: payments +tier: critical +description: Headcount pipeline for payments +schedule: + hour: 1 + days: '*' +owners: +- sam +steps: +- name: extract + table: payments.raw_headcount_00 +- name: validate + table: payments.int_headcount_00 + depends_on: + - extract +- name: transform + table: payments.int_headcount_00 + depends_on: + - validate +- name: aggregate + table: payments.int_headcount_00 + depends_on: + - transform + - validate +- name: snapshot + table: payments.int_headcount_00 + depends_on: + - aggregate +- name: publish + table: payments.mart_headcount_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_products.yaml new file mode 100644 index 0000000..66dfddd --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_products.yaml @@ -0,0 +1,23 @@ +name: payments_products +team: payments +tier: standard +description: Products pipeline for payments +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: payments.raw_products_00 +- name: enrich + table: payments.int_products_00 + depends_on: + - extract +- name: aggregate + table: payments.int_products_00 + depends_on: + - enrich + - extract +- name: publish + table: payments.mart_products_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions.yaml new file mode 100644 index 0000000..cb8ee8e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions.yaml @@ -0,0 +1,26 @@ +name: payments_promotions +team: payments +tier: standard +description: Promotions pipeline for payments +schedule: + hour: 1 + days: 1-5 +steps: +- name: extract + table: payments.raw_promotions_00 +- name: stage + table: payments.stg_promotions_00 + depends_on: + - extract +- name: validate + table: payments.int_promotions_00 + depends_on: + - stage +- name: snapshot + table: payments.int_promotions_00 + depends_on: + - validate +- name: publish + table: payments.mart_promotions_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions_us.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions_us.yaml new file mode 100644 index 0000000..49f8fd7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_promotions_us.yaml @@ -0,0 +1,19 @@ +name: payments_promotions_us +team: payments +tier: standard +description: Promotions pipeline for payments +schedule: + hour: 0 + days: '1' +retries: 4 +steps: +- name: extract + table: payments.raw_promotions_00 +- name: validate + table: payments.int_promotions_00 + depends_on: + - extract +- name: publish + table: payments.mart_promotions_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_refunds.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_refunds.yaml new file mode 100644 index 0000000..3ccdad8 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_refunds.yaml @@ -0,0 +1,28 @@ +name: payments_refunds +team: payments +tier: standard +description: Refunds pipeline for payments +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: payments.raw_refunds_00 +- name: stage + table: payments.stg_refunds_00 + depends_on: + - extract +- name: enrich + table: payments.int_refunds_00 + depends_on: + - stage +- name: aggregate + table: payments.int_refunds_00 + depends_on: + - enrich + - stage +- name: publish + table: payments.mart_refunds_00 + depends_on: + - aggregate + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_sessions.yaml new file mode 100644 index 0000000..ad29516 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_sessions.yaml @@ -0,0 +1,27 @@ +name: payments_sessions +team: payments +tier: standard +description: Sessions pipeline for payments +schedule: + hour: 4 + days: '*' +steps: +- name: extract + table: payments.raw_sessions_00 +- name: transform + table: payments.int_sessions_00 + depends_on: + - extract +- name: enrich + table: payments.int_sessions_00 + depends_on: + - transform + - extract +- name: aggregate + table: payments.int_sessions_00 + depends_on: + - enrich +- name: publish + table: payments.mart_sessions_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_spend.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_spend.yaml new file mode 100644 index 0000000..04d4242 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/payments_spend.yaml @@ -0,0 +1,23 @@ +name: payments_spend +team: payments +tier: standard +description: Spend pipeline for payments +schedule: + hour: 1 + days: '*' +retries: 1 +steps: +- name: extract + table: payments.raw_spend_00 +- name: transform + table: payments.int_spend_00 + depends_on: + - extract +- name: enrich + table: payments.int_spend_00 + depends_on: + - transform +- name: publish + table: payments.mart_spend_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_attribution.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_attribution.yaml new file mode 100644 index 0000000..659cadc --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_attribution.yaml @@ -0,0 +1,21 @@ +name: people_attribution +team: people +tier: standard +description: Attribution pipeline for people +schedule: + hour: 2 + days: '1' +owners: +- quinn +- tara +steps: +- name: extract + table: people.raw_attribution_00 +- name: validate + table: people.int_attribution_00 + depends_on: + - extract +- name: publish + table: people.mart_attribution_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_carriers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_carriers.yaml new file mode 100644 index 0000000..5239c62 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_carriers.yaml @@ -0,0 +1,21 @@ +name: people_carriers +team: people +tier: critical +description: Carriers pipeline for people +schedule: + hour: 6 + days: '1' + minute: 30 +retries: 0 +steps: +- name: extract + table: people.raw_carriers_00 +- name: transform + table: people.int_carriers_00 + depends_on: + - extract +- name: publish + table: people.mart_carriers_00 + depends_on: + - transform + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks.yaml new file mode 100644 index 0000000..be4f17d --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks.yaml @@ -0,0 +1,27 @@ +name: people_chargebacks +team: people +tier: standard +description: Chargebacks pipeline for people +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: people.raw_chargebacks_00 +- name: validate + table: people.int_chargebacks_00 + depends_on: + - extract +- name: transform + table: people.int_chargebacks_00 + depends_on: + - validate +- name: enrich + table: people.int_chargebacks_00 + depends_on: + - transform + - validate +- name: publish + table: people.mart_chargebacks_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks_backfill_safe.yaml new file mode 100644 index 0000000..6f652c4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_chargebacks_backfill_safe.yaml @@ -0,0 +1,27 @@ +name: people_chargebacks_backfill_safe +team: people +tier: standard +description: Chargebacks pipeline for people +schedule: + hour: 4 + days: '*' +steps: +- name: extract + table: people.raw_chargebacks_00 +- name: transform + table: people.int_chargebacks_00 + depends_on: + - extract +- name: aggregate + table: people.int_chargebacks_00 + depends_on: + - transform + - extract +- name: snapshot + table: people.int_chargebacks_00 + depends_on: + - aggregate +- name: publish + table: people.mart_chargebacks_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks.yaml new file mode 100644 index 0000000..7a08e4d --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks.yaml @@ -0,0 +1,18 @@ +name: people_clicks +team: people +tier: standard +description: Clicks pipeline for people +schedule: + hour: 5 + days: 1-5 +steps: +- name: extract + table: people.raw_clicks_00 +- name: enrich + table: people.int_clicks_00 + depends_on: + - extract +- name: publish + table: people.mart_clicks_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks_eu.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks_eu.yaml new file mode 100644 index 0000000..fd0ee9a --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_clicks_eu.yaml @@ -0,0 +1,23 @@ +name: people_clicks_eu +team: people +tier: standard +description: Clicks pipeline for people +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: people.raw_clicks_00 +- name: stage + table: people.stg_clicks_00 + depends_on: + - extract +- name: snapshot + table: people.int_clicks_00 + depends_on: + - stage +- name: publish + table: people.mart_clicks_00 + depends_on: + - snapshot + - stage diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_csat.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_csat.yaml new file mode 100644 index 0000000..58beb6b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_csat.yaml @@ -0,0 +1,37 @@ +name: people_csat +team: people +tier: standard +description: Csat pipeline for people +schedule: + hour: 1 + days: 1-5 +retries: 0 +steps: +- name: extract + table: people.raw_csat_00 +- name: stage + table: people.stg_csat_00 + depends_on: + - extract +- name: transform + table: people.int_csat_00 + depends_on: + - stage +- name: enrich + table: people.int_csat_00 + depends_on: + - transform + - stage +- name: aggregate + table: people.int_csat_00 + depends_on: + - enrich +- name: snapshot + table: people.int_csat_00 + depends_on: + - aggregate +- name: publish + table: people.mart_csat_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_payouts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_payouts.yaml new file mode 100644 index 0000000..67fac88 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_payouts.yaml @@ -0,0 +1,31 @@ +name: people_payouts +team: people +tier: standard +description: Payouts pipeline for people +schedule: + hour: 5 + days: '*' +steps: +- name: extract + table: people.raw_payouts_00 +- name: stage + table: people.stg_payouts_00 + depends_on: + - extract +- name: validate + table: people.int_payouts_00 + depends_on: + - stage +- name: transform + table: people.int_payouts_00 + depends_on: + - validate +- name: aggregate + table: people.int_payouts_00 + depends_on: + - transform +- name: publish + table: people.mart_payouts_00 + depends_on: + - aggregate + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_prices.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_prices.yaml new file mode 100644 index 0000000..6f291be --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_prices.yaml @@ -0,0 +1,30 @@ +name: people_prices +team: people +tier: standard +description: Prices pipeline for people +schedule: + hour: 5 + days: '*' +steps: +- name: extract + table: people.raw_prices_00 +- name: validate + table: people.int_prices_00 + depends_on: + - extract +- name: transform + table: people.int_prices_00 + depends_on: + - validate +- name: enrich + table: people.int_prices_00 + depends_on: + - transform +- name: aggregate + table: people.int_prices_00 + depends_on: + - enrich +- name: publish + table: people.mart_prices_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_products.yaml new file mode 100644 index 0000000..a4b48d8 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_products.yaml @@ -0,0 +1,24 @@ +name: people_products +team: people +tier: standard +description: Products pipeline for people +schedule: + hour: 0 + days: 1-5 +steps: +- name: extract + table: people.raw_products_00 +- name: enrich + table: people.int_products_00 + depends_on: + - extract +- name: aggregate + table: people.int_products_00 + depends_on: + - enrich + - extract +- name: publish + table: people.mart_products_00 + depends_on: + - aggregate + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_sessions.yaml new file mode 100644 index 0000000..034c508 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/people_sessions.yaml @@ -0,0 +1,23 @@ +name: people_sessions +team: people +tier: standard +description: Sessions pipeline for people +schedule: + hour: 0 + days: 1-5 +retries: 1 +steps: +- name: extract + table: people.raw_sessions_00 +- name: transform + table: people.int_sessions_00 + depends_on: + - extract +- name: aggregate + table: people.int_sessions_00 + depends_on: + - transform +- name: publish + table: people.mart_sessions_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_carriers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_carriers.yaml new file mode 100644 index 0000000..eab638e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_carriers.yaml @@ -0,0 +1,24 @@ +name: risk_carriers +team: risk +tier: standard +description: Carriers pipeline for risk +schedule: + hour: 1 + days: '*' + minute: 30 +retries: 4 +steps: +- name: extract + table: risk.raw_carriers_00 +- name: transform + table: risk.int_carriers_00 + depends_on: + - extract +- name: enrich + table: risk.int_carriers_00 + depends_on: + - transform +- name: publish + table: risk.mart_carriers_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_cohorts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_cohorts.yaml new file mode 100644 index 0000000..2c9be75 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_cohorts.yaml @@ -0,0 +1,36 @@ +name: risk_cohorts +team: risk +tier: standard +description: Cohorts pipeline for risk +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: risk.raw_cohorts_00 +- name: stage + table: risk.stg_cohorts_00 + depends_on: + - extract +- name: validate + table: risk.int_cohorts_00 + depends_on: + - stage +- name: enrich + table: risk.int_cohorts_00 + depends_on: + - validate +- name: aggregate + table: risk.int_cohorts_00 + depends_on: + - enrich + - validate +- name: snapshot + table: risk.int_cohorts_00 + depends_on: + - aggregate + - enrich +- name: publish + table: risk.mart_cohorts_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_fraud_scores.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_fraud_scores.yaml new file mode 100644 index 0000000..dce96b5 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_fraud_scores.yaml @@ -0,0 +1,32 @@ +name: risk_fraud_scores +team: risk +tier: standard +description: Fraud Scores pipeline for risk +schedule: + hour: 2 + days: 1-5 +steps: +- name: extract + table: risk.raw_fraud_scores_00 +- name: stage + table: risk.stg_fraud_scores_00 + depends_on: + - extract +- name: transform + table: risk.int_fraud_scores_00 + depends_on: + - stage + - extract +- name: enrich + table: risk.int_fraud_scores_00 + depends_on: + - transform + - stage +- name: aggregate + table: risk.int_fraud_scores_00 + depends_on: + - enrich +- name: publish + table: risk.mart_fraud_scores_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_headcount.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_headcount.yaml new file mode 100644 index 0000000..a327997 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_headcount.yaml @@ -0,0 +1,20 @@ +name: risk_headcount +team: risk +tier: standard +description: Headcount pipeline for risk +schedule: + hour: 1 + days: 1-5 +retries: 4 +steps: +- name: extract + table: risk.raw_headcount_00 +- name: enrich + table: risk.int_headcount_00 + depends_on: + - extract +- name: publish + table: risk.mart_headcount_00 + depends_on: + - enrich + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders.yaml new file mode 100644 index 0000000..1b8094b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders.yaml @@ -0,0 +1,31 @@ +name: risk_orders +team: risk +tier: critical +description: Orders pipeline for risk +schedule: + hour: 6 + days: 1-5 +steps: +- name: extract + table: risk.raw_orders_00 +- name: validate + table: risk.int_orders_00 + depends_on: + - extract +- name: enrich + table: risk.int_orders_00 + depends_on: + - validate +- name: aggregate + table: risk.int_orders_00 + depends_on: + - enrich +- name: snapshot + table: risk.int_orders_00 + depends_on: + - aggregate +- name: publish + table: risk.mart_orders_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders_daily.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders_daily.yaml new file mode 100644 index 0000000..eb4e98d --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_orders_daily.yaml @@ -0,0 +1,24 @@ +name: risk_orders_daily +team: risk +tier: standard +description: Orders pipeline for risk +schedule: + hour: 3 + days: '1' +retries: 4 +steps: +- name: extract + table: risk.raw_orders_00 +- name: stage + table: risk.stg_orders_00 + depends_on: + - extract +- name: enrich + table: risk.int_orders_00 + depends_on: + - stage +- name: publish + table: risk.mart_orders_00 + depends_on: + - enrich + - stage diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_products.yaml new file mode 100644 index 0000000..cdeb4c0 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_products.yaml @@ -0,0 +1,30 @@ +name: risk_products +team: risk +tier: critical +description: Products pipeline for risk +schedule: + hour: 4 + days: 1-5 +owners: +- jo +- yusuf +steps: +- name: extract + table: risk.raw_products_00 +- name: enrich + table: risk.int_products_00 + depends_on: + - extract +- name: aggregate + table: risk.int_products_00 + depends_on: + - enrich + - extract +- name: snapshot + table: risk.int_products_00 + depends_on: + - aggregate +- name: publish + table: risk.mart_products_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_refunds.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_refunds.yaml new file mode 100644 index 0000000..e8a71e4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_refunds.yaml @@ -0,0 +1,24 @@ +name: risk_refunds +team: risk +tier: standard +description: Refunds pipeline for risk +schedule: + hour: 4 + days: '*' + minute: 0 +steps: +- name: extract + table: risk.raw_refunds_00 +- name: stage + table: risk.stg_refunds_00 + depends_on: + - extract +- name: enrich + table: risk.int_refunds_00 + depends_on: + - stage + - extract +- name: publish + table: risk.mart_refunds_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend.yaml new file mode 100644 index 0000000..0ea2bf7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend.yaml @@ -0,0 +1,28 @@ +name: risk_spend +team: risk +tier: standard +description: Spend pipeline for risk +schedule: + hour: 4 + days: '*' +retries: 4 +steps: +- name: extract + table: risk.raw_spend_00 +- name: stage + table: risk.stg_spend_00 + depends_on: + - extract +- name: validate + table: risk.int_spend_00 + depends_on: + - stage + - extract +- name: enrich + table: risk.int_spend_00 + depends_on: + - validate +- name: publish + table: risk.mart_spend_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend_eu.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend_eu.yaml new file mode 100644 index 0000000..f349398 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_spend_eu.yaml @@ -0,0 +1,25 @@ +name: risk_spend_eu +team: risk +tier: standard +description: Spend pipeline for risk +schedule: + hour: 0 + days: '*' +owners: +- uma +steps: +- name: extract + table: risk.raw_spend_00 +- name: transform + table: risk.int_spend_00 + depends_on: + - extract +- name: snapshot + table: risk.int_spend_00 + depends_on: + - transform + - extract +- name: publish + table: risk.mart_spend_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_tickets.yaml new file mode 100644 index 0000000..a075ff0 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_tickets.yaml @@ -0,0 +1,19 @@ +name: risk_tickets +team: risk +tier: standard +description: Tickets pipeline for risk +schedule: + hour: 5 + days: '1' +retries: 0 +steps: +- name: extract + table: risk.raw_tickets_00 +- name: validate + table: risk.int_tickets_00 + depends_on: + - extract +- name: publish + table: risk.mart_tickets_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_warehouses.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_warehouses.yaml new file mode 100644 index 0000000..13b3c08 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/risk_warehouses.yaml @@ -0,0 +1,33 @@ +name: risk_warehouses +team: risk +tier: standard +description: Warehouses pipeline for risk +schedule: + hour: 5 + days: '1' + minute: 45 +steps: +- name: extract + table: risk.raw_warehouses_00 +- name: transform + table: risk.int_warehouses_00 + depends_on: + - extract +- name: enrich + table: risk.int_warehouses_00 + depends_on: + - transform +- name: aggregate + table: risk.int_warehouses_00 + depends_on: + - enrich + - transform +- name: snapshot + table: risk.int_warehouses_00 + depends_on: + - aggregate +- name: publish + table: risk.mart_warehouses_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_attribution.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_attribution.yaml new file mode 100644 index 0000000..d047ea6 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_attribution.yaml @@ -0,0 +1,32 @@ +name: search_attribution +team: search +tier: standard +description: Attribution pipeline for search +schedule: + hour: 0 + days: '*' +owners: +- uma +steps: +- name: extract + table: search.raw_attribution_00 +- name: stage + table: search.stg_attribution_00 + depends_on: + - extract +- name: transform + table: search.int_attribution_00 + depends_on: + - stage +- name: aggregate + table: search.int_attribution_00 + depends_on: + - transform +- name: snapshot + table: search.int_attribution_00 + depends_on: + - aggregate +- name: publish + table: search.mart_attribution_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_campaigns.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_campaigns.yaml new file mode 100644 index 0000000..02a1ce2 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_campaigns.yaml @@ -0,0 +1,24 @@ +name: search_campaigns +team: search +tier: standard +description: Campaigns pipeline for search +schedule: + hour: 5 + days: 1-5 +owners: +- ivan +steps: +- name: extract + table: search.raw_campaigns_00 +- name: transform + table: search.int_campaigns_00 + depends_on: + - extract +- name: snapshot + table: search.int_campaigns_00 + depends_on: + - transform +- name: publish + table: search.mart_campaigns_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_chargebacks.yaml new file mode 100644 index 0000000..66a7365 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_chargebacks.yaml @@ -0,0 +1,32 @@ +name: search_chargebacks +team: search +tier: standard +description: Chargebacks pipeline for search +schedule: + hour: 3 + days: 1-5 + minute: 15 +steps: +- name: extract + table: search.raw_chargebacks_00 +- name: transform + table: search.int_chargebacks_00 + depends_on: + - extract +- name: enrich + table: search.int_chargebacks_00 + depends_on: + - transform + - extract +- name: aggregate + table: search.int_chargebacks_00 + depends_on: + - enrich +- name: snapshot + table: search.int_chargebacks_00 + depends_on: + - aggregate +- name: publish + table: search.mart_chargebacks_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_forecast.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_forecast.yaml new file mode 100644 index 0000000..904b83f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_forecast.yaml @@ -0,0 +1,18 @@ +name: search_forecast +team: search +tier: standard +description: Forecast pipeline for search +schedule: + hour: 3 + days: '1' +steps: +- name: extract + table: search.raw_forecast_00 +- name: snapshot + table: search.int_forecast_00 + depends_on: + - extract +- name: publish + table: search.mart_forecast_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_inventory.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_inventory.yaml new file mode 100644 index 0000000..27092a4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_inventory.yaml @@ -0,0 +1,35 @@ +name: search_inventory +team: search +tier: standard +description: Inventory pipeline for search +schedule: + hour: 1 + days: '*' +steps: +- name: extract + table: search.raw_inventory_00 +- name: stage + table: search.stg_inventory_00 + depends_on: + - extract +- name: transform + table: search.int_inventory_00 + depends_on: + - stage +- name: enrich + table: search.int_inventory_00 + depends_on: + - transform +- name: aggregate + table: search.int_inventory_00 + depends_on: + - enrich +- name: snapshot + table: search.int_inventory_00 + depends_on: + - aggregate + - enrich +- name: publish + table: search.mart_inventory_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_products.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_products.yaml new file mode 100644 index 0000000..741d94e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_products.yaml @@ -0,0 +1,31 @@ +name: search_products +team: search +tier: standard +description: Products pipeline for search +schedule: + hour: 0 + days: '*' +retries: 0 +steps: +- name: extract + table: search.raw_products_00 +- name: validate + table: search.int_products_00 + depends_on: + - extract +- name: transform + table: search.int_products_00 + depends_on: + - validate +- name: enrich + table: search.int_products_00 + depends_on: + - transform +- name: aggregate + table: search.int_products_00 + depends_on: + - enrich +- name: publish + table: search.mart_products_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_refunds.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_refunds.yaml new file mode 100644 index 0000000..706685f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_refunds.yaml @@ -0,0 +1,27 @@ +name: search_refunds +team: search +tier: standard +description: Refunds pipeline for search +schedule: + hour: 1 + days: 1-5 +steps: +- name: extract + table: search.raw_refunds_00 +- name: validate + table: search.int_refunds_00 + depends_on: + - extract +- name: enrich + table: search.int_refunds_00 + depends_on: + - validate + - extract +- name: aggregate + table: search.int_refunds_00 + depends_on: + - enrich +- name: publish + table: search.mart_refunds_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups.yaml new file mode 100644 index 0000000..e57464b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups.yaml @@ -0,0 +1,31 @@ +name: search_signups +team: search +tier: standard +description: Signups pipeline for search +schedule: + hour: 4 + days: 1-5 +owners: +- vik +- quinn +retries: 1 +steps: +- name: extract + table: search.raw_signups_00 +- name: validate + table: search.int_signups_00 + depends_on: + - extract +- name: transform + table: search.int_signups_00 + depends_on: + - validate +- name: aggregate + table: search.int_signups_00 + depends_on: + - transform +- name: publish + table: search.mart_signups_00 + depends_on: + - aggregate + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_backfill_safe.yaml new file mode 100644 index 0000000..5b4d1ca --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_backfill_safe.yaml @@ -0,0 +1,37 @@ +name: search_signups_backfill_safe +team: search +tier: standard +description: Signups pipeline for search +schedule: + hour: 4 + days: 1-5 + minute: 45 +retries: 4 +steps: +- name: extract + table: search.raw_signups_00 +- name: validate + table: search.int_signups_00 + depends_on: + - extract +- name: transform + table: search.int_signups_00 + depends_on: + - validate +- name: enrich + table: search.int_signups_00 + depends_on: + - transform +- name: aggregate + table: search.int_signups_00 + depends_on: + - enrich + - transform +- name: snapshot + table: search.int_signups_00 + depends_on: + - aggregate +- name: publish + table: search.mart_signups_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_eu.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_eu.yaml new file mode 100644 index 0000000..e37f6d8 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_signups_eu.yaml @@ -0,0 +1,35 @@ +name: search_signups_eu +team: search +tier: standard +description: Signups pipeline for search +schedule: + hour: 5 + days: '1' +steps: +- name: extract + table: search.raw_signups_00 +- name: stage + table: search.stg_signups_00 + depends_on: + - extract +- name: validate + table: search.int_signups_00 + depends_on: + - stage +- name: enrich + table: search.int_signups_00 + depends_on: + - validate + - stage +- name: aggregate + table: search.int_signups_00 + depends_on: + - enrich +- name: snapshot + table: search.int_signups_00 + depends_on: + - aggregate +- name: publish + table: search.mart_signups_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_suppliers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_suppliers.yaml new file mode 100644 index 0000000..b5a7964 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/search_suppliers.yaml @@ -0,0 +1,24 @@ +name: search_suppliers +team: search +tier: standard +description: Suppliers pipeline for search +schedule: + hour: 4 + days: '*' +steps: +- name: extract + table: search.raw_suppliers_00 +- name: transform + table: search.int_suppliers_00 + depends_on: + - extract +- name: enrich + table: search.int_suppliers_00 + depends_on: + - transform + - extract +- name: publish + table: search.mart_suppliers_00 + depends_on: + - enrich + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_attribution.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_attribution.yaml new file mode 100644 index 0000000..c963353 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_attribution.yaml @@ -0,0 +1,19 @@ +name: supply_attribution +team: supply +tier: standard +description: Attribution pipeline for supply +schedule: + hour: 2 + days: '1' +retries: 0 +steps: +- name: extract + table: supply.raw_attribution_00 +- name: validate + table: supply.int_attribution_00 + depends_on: + - extract +- name: publish + table: supply.mart_attribution_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks.yaml new file mode 100644 index 0000000..96c1838 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks.yaml @@ -0,0 +1,26 @@ +name: supply_chargebacks +team: supply +tier: standard +description: Chargebacks pipeline for supply +schedule: + hour: 4 + days: '1' +steps: +- name: extract + table: supply.raw_chargebacks_00 +- name: stage + table: supply.stg_chargebacks_00 + depends_on: + - extract +- name: validate + table: supply.int_chargebacks_00 + depends_on: + - stage +- name: transform + table: supply.int_chargebacks_00 + depends_on: + - validate +- name: publish + table: supply.mart_chargebacks_00 + depends_on: + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks_hourly_rollup.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks_hourly_rollup.yaml new file mode 100644 index 0000000..b094068 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_chargebacks_hourly_rollup.yaml @@ -0,0 +1,25 @@ +name: supply_chargebacks_hourly_rollup +team: supply +tier: standard +description: Chargebacks pipeline for supply +schedule: + hour: 0 + days: '*' +owners: +- rosa +steps: +- name: extract + table: supply.raw_chargebacks_00 +- name: validate + table: supply.int_chargebacks_00 + depends_on: + - extract +- name: snapshot + table: supply.int_chargebacks_00 + depends_on: + - validate +- name: publish + table: supply.mart_chargebacks_00 + depends_on: + - snapshot + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_clicks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_clicks.yaml new file mode 100644 index 0000000..1fd4972 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_clicks.yaml @@ -0,0 +1,32 @@ +name: supply_clicks +team: supply +tier: standard +description: Clicks pipeline for supply +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: supply.raw_clicks_00 +- name: stage + table: supply.stg_clicks_00 + depends_on: + - extract +- name: enrich + table: supply.int_clicks_00 + depends_on: + - stage + - extract +- name: aggregate + table: supply.int_clicks_00 + depends_on: + - enrich +- name: snapshot + table: supply.int_clicks_00 + depends_on: + - aggregate +- name: publish + table: supply.mart_clicks_00 + depends_on: + - snapshot + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_cohorts.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_cohorts.yaml new file mode 100644 index 0000000..8948004 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_cohorts.yaml @@ -0,0 +1,21 @@ +name: supply_cohorts +team: supply +tier: standard +description: Cohorts pipeline for supply +schedule: + hour: 4 + days: '1' +owners: +- fatima +steps: +- name: extract + table: supply.raw_cohorts_00 +- name: aggregate + table: supply.int_cohorts_00 + depends_on: + - extract +- name: publish + table: supply.mart_cohorts_00 + depends_on: + - aggregate + - extract diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_orders.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_orders.yaml new file mode 100644 index 0000000..e6ef8c4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_orders.yaml @@ -0,0 +1,22 @@ +name: supply_orders +team: supply +tier: standard +description: Orders pipeline for supply +schedule: + hour: 4 + days: '*' +steps: +- name: extract + table: supply.raw_orders_00 +- name: transform + table: supply.int_orders_00 + depends_on: + - extract +- name: enrich + table: supply.int_orders_00 + depends_on: + - transform +- name: publish + table: supply.mart_orders_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns.yaml new file mode 100644 index 0000000..daf1c55 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns.yaml @@ -0,0 +1,36 @@ +name: supply_returns +team: supply +tier: standard +description: Returns pipeline for supply +schedule: + hour: 2 + days: '*' +retries: 1 +steps: +- name: extract + table: supply.raw_returns_00 +- name: stage + table: supply.stg_returns_00 + depends_on: + - extract +- name: validate + table: supply.int_returns_00 + depends_on: + - stage + - extract +- name: transform + table: supply.int_returns_00 + depends_on: + - validate +- name: aggregate + table: supply.int_returns_00 + depends_on: + - transform +- name: snapshot + table: supply.int_returns_00 + depends_on: + - aggregate +- name: publish + table: supply.mart_returns_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns_us.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns_us.yaml new file mode 100644 index 0000000..83629f5 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_returns_us.yaml @@ -0,0 +1,23 @@ +name: supply_returns_us +team: supply +tier: standard +description: Returns pipeline for supply +schedule: + hour: 2 + days: '*' + minute: 15 +steps: +- name: extract + table: supply.raw_returns_00 +- name: validate + table: supply.int_returns_00 + depends_on: + - extract +- name: enrich + table: supply.int_returns_00 + depends_on: + - validate +- name: publish + table: supply.mart_returns_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions.yaml new file mode 100644 index 0000000..9f795f2 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions.yaml @@ -0,0 +1,32 @@ +name: supply_sessions +team: supply +tier: standard +description: Sessions pipeline for supply +schedule: + hour: 5 + days: '*' +owners: +- yusuf +steps: +- name: extract + table: supply.raw_sessions_00 +- name: stage + table: supply.stg_sessions_00 + depends_on: + - extract +- name: transform + table: supply.int_sessions_00 + depends_on: + - stage +- name: enrich + table: supply.int_sessions_00 + depends_on: + - transform +- name: aggregate + table: supply.int_sessions_00 + depends_on: + - enrich +- name: publish + table: supply.mart_sessions_00 + depends_on: + - aggregate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions_us.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions_us.yaml new file mode 100644 index 0000000..e008380 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_sessions_us.yaml @@ -0,0 +1,30 @@ +name: supply_sessions_us +team: supply +tier: standard +description: Sessions pipeline for supply +schedule: + hour: 4 + days: '*' +steps: +- name: extract + table: supply.raw_sessions_00 +- name: stage + table: supply.stg_sessions_00 + depends_on: + - extract +- name: validate + table: supply.int_sessions_00 + depends_on: + - stage +- name: aggregate + table: supply.int_sessions_00 + depends_on: + - validate +- name: snapshot + table: supply.int_sessions_00 + depends_on: + - aggregate +- name: publish + table: supply.mart_sessions_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_spend.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_spend.yaml new file mode 100644 index 0000000..5e4da3c --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_spend.yaml @@ -0,0 +1,31 @@ +name: supply_spend +team: supply +tier: standard +description: Spend pipeline for supply +schedule: + hour: 3 + days: '*' +steps: +- name: extract + table: supply.raw_spend_00 +- name: stage + table: supply.stg_spend_00 + depends_on: + - extract +- name: validate + table: supply.int_spend_00 + depends_on: + - stage + - extract +- name: transform + table: supply.int_spend_00 + depends_on: + - validate +- name: enrich + table: supply.int_spend_00 + depends_on: + - transform +- name: publish + table: supply.mart_spend_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_tickets.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_tickets.yaml new file mode 100644 index 0000000..030a399 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/supply_tickets.yaml @@ -0,0 +1,35 @@ +name: supply_tickets +team: supply +tier: critical +description: Tickets pipeline for supply +schedule: + hour: 4 + days: '*' +owners: +- fatima +- uma +steps: +- name: extract + table: supply.raw_tickets_00 +- name: validate + table: supply.int_tickets_00 + depends_on: + - extract +- name: transform + table: supply.int_tickets_00 + depends_on: + - validate + - extract +- name: enrich + table: supply.int_tickets_00 + depends_on: + - transform +- name: aggregate + table: supply.int_tickets_00 + depends_on: + - enrich +- name: publish + table: supply.mart_tickets_00 + depends_on: + - aggregate + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_carriers.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_carriers.yaml new file mode 100644 index 0000000..8e3fb79 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_carriers.yaml @@ -0,0 +1,31 @@ +name: support_carriers +team: support +tier: standard +description: Carriers pipeline for support +schedule: + hour: 6 + days: '*' +steps: +- name: extract + table: support.raw_carriers_00 +- name: stage + table: support.stg_carriers_00 + depends_on: + - extract +- name: validate + table: support.int_carriers_00 + depends_on: + - stage +- name: enrich + table: support.int_carriers_00 + depends_on: + - validate + - stage +- name: snapshot + table: support.int_carriers_00 + depends_on: + - enrich +- name: publish + table: support.mart_carriers_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_chargebacks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_chargebacks.yaml new file mode 100644 index 0000000..8ad62e9 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_chargebacks.yaml @@ -0,0 +1,31 @@ +name: support_chargebacks +team: support +tier: standard +description: Chargebacks pipeline for support +schedule: + hour: 3 + days: '*' + minute: 0 +owners: +- vik +- pia +retries: 1 +steps: +- name: extract + table: support.raw_chargebacks_00 +- name: stage + table: support.stg_chargebacks_00 + depends_on: + - extract +- name: transform + table: support.int_chargebacks_00 + depends_on: + - stage +- name: enrich + table: support.int_chargebacks_00 + depends_on: + - transform +- name: publish + table: support.mart_chargebacks_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_clicks.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_clicks.yaml new file mode 100644 index 0000000..bd18d73 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_clicks.yaml @@ -0,0 +1,34 @@ +name: support_clicks +team: support +tier: standard +description: Clicks pipeline for support +schedule: + hour: 6 + days: '*' +owners: +- vik +retries: 1 +steps: +- name: extract + table: support.raw_clicks_00 +- name: stage + table: support.stg_clicks_00 + depends_on: + - extract +- name: validate + table: support.int_clicks_00 + depends_on: + - stage +- name: transform + table: support.int_clicks_00 + depends_on: + - validate +- name: enrich + table: support.int_clicks_00 + depends_on: + - transform +- name: publish + table: support.mart_clicks_00 + depends_on: + - enrich + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast.yaml new file mode 100644 index 0000000..11a3144 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast.yaml @@ -0,0 +1,32 @@ +name: support_forecast +team: support +tier: critical +description: Forecast pipeline for support +schedule: + hour: 4 + days: '*' +retries: 1 +steps: +- name: extract + table: support.raw_forecast_00 +- name: stage + table: support.stg_forecast_00 + depends_on: + - extract +- name: validate + table: support.int_forecast_00 + depends_on: + - stage +- name: enrich + table: support.int_forecast_00 + depends_on: + - validate +- name: snapshot + table: support.int_forecast_00 + depends_on: + - enrich +- name: publish + table: support.mart_forecast_00 + depends_on: + - snapshot + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_backfill_safe.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_backfill_safe.yaml new file mode 100644 index 0000000..10513f7 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_backfill_safe.yaml @@ -0,0 +1,23 @@ +name: support_forecast_backfill_safe +team: support +tier: standard +description: Forecast pipeline for support +schedule: + hour: 1 + days: '*' + minute: 30 +steps: +- name: extract + table: support.raw_forecast_00 +- name: stage + table: support.stg_forecast_00 + depends_on: + - extract +- name: snapshot + table: support.int_forecast_00 + depends_on: + - stage +- name: publish + table: support.mart_forecast_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_daily.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_daily.yaml new file mode 100644 index 0000000..e98655f --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_daily.yaml @@ -0,0 +1,36 @@ +name: support_forecast_daily +team: support +tier: standard +description: Forecast pipeline for support +schedule: + hour: 0 + days: '*' +steps: +- name: extract + table: support.raw_forecast_00 +- name: stage + table: support.stg_forecast_00 + depends_on: + - extract +- name: validate + table: support.int_forecast_00 + depends_on: + - stage +- name: transform + table: support.int_forecast_00 + depends_on: + - validate +- name: enrich + table: support.int_forecast_00 + depends_on: + - transform + - validate +- name: snapshot + table: support.int_forecast_00 + depends_on: + - enrich +- name: publish + table: support.mart_forecast_00 + depends_on: + - snapshot + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_v2.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_v2.yaml new file mode 100644 index 0000000..81ee595 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_forecast_v2.yaml @@ -0,0 +1,30 @@ +name: support_forecast_v2 +team: support +tier: standard +description: Forecast pipeline for support +schedule: + hour: 1 + days: '1' +steps: +- name: extract + table: support.raw_forecast_00 +- name: transform + table: support.int_forecast_00 + depends_on: + - extract +- name: enrich + table: support.int_forecast_00 + depends_on: + - transform +- name: aggregate + table: support.int_forecast_00 + depends_on: + - enrich +- name: snapshot + table: support.int_forecast_00 + depends_on: + - aggregate +- name: publish + table: support.mart_forecast_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_fraud_scores.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_fraud_scores.yaml new file mode 100644 index 0000000..7b08b38 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_fraud_scores.yaml @@ -0,0 +1,34 @@ +name: support_fraud_scores +team: support +tier: standard +description: Fraud Scores pipeline for support +schedule: + hour: 5 + days: '1' +owners: +- gus +- ben +retries: 1 +steps: +- name: extract + table: support.raw_fraud_scores_00 +- name: stage + table: support.stg_fraud_scores_00 + depends_on: + - extract +- name: transform + table: support.int_fraud_scores_00 + depends_on: + - stage +- name: aggregate + table: support.int_fraud_scores_00 + depends_on: + - transform +- name: snapshot + table: support.int_fraud_scores_00 + depends_on: + - aggregate +- name: publish + table: support.mart_fraud_scores_00 + depends_on: + - snapshot diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_inventory.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_inventory.yaml new file mode 100644 index 0000000..0d29686 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_inventory.yaml @@ -0,0 +1,18 @@ +name: support_inventory +team: support +tier: standard +description: Inventory pipeline for support +schedule: + hour: 3 + days: '1' +steps: +- name: extract + table: support.raw_inventory_00 +- name: validate + table: support.int_inventory_00 + depends_on: + - extract +- name: publish + table: support.mart_inventory_00 + depends_on: + - validate diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_payroll.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_payroll.yaml new file mode 100644 index 0000000..6d0790b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_payroll.yaml @@ -0,0 +1,28 @@ +name: support_payroll +team: support +tier: standard +description: Payroll pipeline for support +schedule: + hour: 2 + days: '*' +steps: +- name: extract + table: support.raw_payroll_00 +- name: validate + table: support.int_payroll_00 + depends_on: + - extract +- name: transform + table: support.int_payroll_00 + depends_on: + - validate + - extract +- name: aggregate + table: support.int_payroll_00 + depends_on: + - transform +- name: publish + table: support.mart_payroll_00 + depends_on: + - aggregate + - transform diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_sessions.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_sessions.yaml new file mode 100644 index 0000000..29e69d0 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/pipelines/support_sessions.yaml @@ -0,0 +1,23 @@ +name: support_sessions +team: support +tier: standard +description: Sessions pipeline for support +schedule: + hour: 3 + days: '*' +retries: 0 +steps: +- name: extract + table: support.raw_sessions_00 +- name: stage + table: support.stg_sessions_00 + depends_on: + - extract +- name: enrich + table: support.int_sessions_00 + depends_on: + - stage +- name: publish + table: support.mart_sessions_00 + depends_on: + - enrich diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/registry.yaml b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/registry.yaml new file mode 100644 index 0000000..35ced6b --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/configs/registry.yaml @@ -0,0 +1,20273 @@ +# Team and dataset registry, exported from the platform catalog on every deploy. +version: 41 +generated_by: platform-catalog-export +teams: + billing: + owners: + - vik + - rosa + - ivan + oncall: billing-oncall@example.com + slack: '#billing-data-alerts' + cost_center: CC-1523 + default_retries: 1 + finance: + owners: + - dara + - omar + - lena + oncall: finance-oncall@example.com + slack: '#finance-data-alerts' + cost_center: CC-4207 + default_retries: 3 + growth: + owners: + - kai + - quinn + - ben + oncall: growth-oncall@example.com + slack: '#growth-data-alerts' + cost_center: CC-8580 + default_retries: 1 + marketing: + owners: + - ivan + - vik + - wen + oncall: marketing-oncall@example.com + slack: '#marketing-data-alerts' + cost_center: CC-7591 + default_retries: 3 + risk: + owners: + - sam + - vik + - quinn + oncall: risk-oncall@example.com + slack: '#risk-data-alerts' + cost_center: CC-2600 + default_retries: 1 + supply: + owners: + - gus + - ivan + - rosa + oncall: supply-oncall@example.com + slack: '#supply-data-alerts' + cost_center: CC-1409 + default_retries: 1 + logistics: + owners: + - pia + - uma + - sam + oncall: logistics-oncall@example.com + slack: '#logistics-data-alerts' + cost_center: CC-5724 + default_retries: 2 + catalog: + owners: + - fatima + - tara + - gus + oncall: catalog-oncall@example.com + slack: '#catalog-data-alerts' + cost_center: CC-3940 + default_retries: 1 + search: + owners: + - dara + - hana + - fatima + oncall: search-oncall@example.com + slack: '#search-data-alerts' + cost_center: CC-6828 + default_retries: 2 + payments: + owners: + - nils + - ana + - vik + oncall: payments-oncall@example.com + slack: '#payments-data-alerts' + cost_center: CC-6703 + default_retries: 3 + support: + owners: + - sam + - vik + - ben + oncall: support-oncall@example.com + slack: '#support-data-alerts' + cost_center: CC-4821 + default_retries: 3 + people: + owners: + - hana + - ana + - tara + oncall: people-oncall@example.com + slack: '#people-data-alerts' + cost_center: CC-8829 + default_retries: 3 +datasets: + billing.raw_orders_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for raw layer + - name: col_01 + type: date + description: orders attribute 1 for raw layer + - name: col_02 + type: date + description: orders attribute 2 for raw layer + billing.stg_orders_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: int + description: orders attribute 1 for stg layer + - name: col_02 + type: timestamp + description: orders attribute 2 for stg layer + billing.int_orders_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for int layer + - name: col_01 + type: int + description: orders attribute 1 for int layer + - name: col_02 + type: timestamp + description: orders attribute 2 for int layer + billing.mart_orders_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for mart layer + - name: col_01 + type: int + description: orders attribute 1 for mart layer + - name: col_02 + type: date + description: orders attribute 2 for mart layer + billing.raw_invoices_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for raw layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for raw layer + billing.stg_invoices_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for stg layer + - name: col_01 + type: date + description: invoices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for stg layer + billing.int_invoices_00: + owner_team: billing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for int layer + - name: col_01 + type: int + description: invoices attribute 1 for int layer + - name: col_02 + type: varchar + description: invoices attribute 2 for int layer + billing.mart_invoices_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for mart layer + - name: col_01 + type: date + description: invoices attribute 1 for mart layer + - name: col_02 + type: date + description: invoices attribute 2 for mart layer + billing.raw_ledger_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for raw layer + - name: col_01 + type: varchar + description: ledger attribute 1 for raw layer + - name: col_02 + type: int + description: ledger attribute 2 for raw layer + billing.stg_ledger_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for stg layer + - name: col_01 + type: int + description: ledger attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for stg layer + billing.int_ledger_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for int layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for int layer + billing.mart_ledger_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for mart layer + - name: col_01 + type: date + description: ledger attribute 1 for mart layer + - name: col_02 + type: int + description: ledger attribute 2 for mart layer + billing.raw_refunds_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for raw layer + - name: col_01 + type: int + description: refunds attribute 1 for raw layer + - name: col_02 + type: varchar + description: refunds attribute 2 for raw layer + billing.stg_refunds_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for stg layer + billing.int_refunds_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: refunds attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for int layer + - name: col_02 + type: varchar + description: refunds attribute 2 for int layer + billing.mart_refunds_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for mart layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for mart layer + billing.raw_sessions_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: date + description: sessions attribute 2 for raw layer + billing.stg_sessions_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: sessions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for stg layer + - name: col_02 + type: varchar + description: sessions attribute 2 for stg layer + billing.int_sessions_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for int layer + - name: col_01 + type: varchar + description: sessions attribute 1 for int layer + - name: col_02 + type: varchar + description: sessions attribute 2 for int layer + billing.mart_sessions_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for mart layer + - name: col_01 + type: int + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + billing.raw_signups_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: signups attribute 0 for raw layer + - name: col_01 + type: int + description: signups attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for raw layer + billing.stg_signups_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for stg layer + - name: col_02 + type: timestamp + description: signups attribute 2 for stg layer + billing.int_signups_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for int layer + - name: col_02 + type: varchar + description: signups attribute 2 for int layer + billing.mart_signups_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for mart layer + - name: col_01 + type: varchar + description: signups attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for mart layer + billing.raw_campaigns_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for raw layer + - name: col_01 + type: date + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + billing.stg_campaigns_00: + owner_team: billing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for stg layer + billing.int_campaigns_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for int layer + - name: col_02 + type: int + description: campaigns attribute 2 for int layer + billing.mart_campaigns_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for mart layer + billing.raw_spend_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: timestamp + description: spend attribute 1 for raw layer + - name: col_02 + type: varchar + description: spend attribute 2 for raw layer + billing.stg_spend_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for stg layer + - name: col_01 + type: int + description: spend attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for stg layer + billing.int_spend_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: varchar + description: spend attribute 1 for int layer + - name: col_02 + type: date + description: spend attribute 2 for int layer + billing.mart_spend_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for mart layer + - name: col_01 + type: timestamp + description: spend attribute 1 for mart layer + - name: col_02 + type: int + description: spend attribute 2 for mart layer + billing.raw_fraud_scores_00: + owner_team: billing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for raw layer + billing.stg_fraud_scores_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for stg layer + billing.int_fraud_scores_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for int layer + billing.mart_fraud_scores_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for mart layer + billing.raw_chargebacks_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: date + description: chargebacks attribute 2 for raw layer + billing.stg_chargebacks_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: date + description: chargebacks attribute 2 for stg layer + billing.int_chargebacks_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for int layer + - name: col_01 + type: int + description: chargebacks attribute 1 for int layer + - name: col_02 + type: int + description: chargebacks attribute 2 for int layer + billing.mart_chargebacks_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: int + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: date + description: chargebacks attribute 2 for mart layer + billing.raw_inventory_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: inventory attribute 0 for raw layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for raw layer + - name: col_02 + type: date + description: inventory attribute 2 for raw layer + billing.stg_inventory_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for stg layer + - name: col_01 + type: int + description: inventory attribute 1 for stg layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for stg layer + billing.int_inventory_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for int layer + - name: col_01 + type: date + description: inventory attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for int layer + billing.mart_inventory_00: + owner_team: billing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: inventory attribute 0 for mart layer + - name: col_01 + type: varchar + description: inventory attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for mart layer + billing.raw_shipments_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for raw layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for raw layer + billing.stg_shipments_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for stg layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for stg layer + - name: col_02 + type: varchar + description: shipments attribute 2 for stg layer + billing.int_shipments_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for int layer + - name: col_02 + type: varchar + description: shipments attribute 2 for int layer + billing.mart_shipments_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: shipments attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for mart layer + billing.raw_carriers_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for raw layer + - name: col_01 + type: int + description: carriers attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for raw layer + billing.stg_carriers_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for stg layer + billing.int_carriers_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for int layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for int layer + - name: col_02 + type: varchar + description: carriers attribute 2 for int layer + billing.mart_carriers_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for mart layer + - name: col_01 + type: int + description: carriers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for mart layer + billing.raw_products_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for raw layer + - name: col_01 + type: date + description: products attribute 1 for raw layer + - name: col_02 + type: int + description: products attribute 2 for raw layer + billing.stg_products_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for stg layer + - name: col_01 + type: timestamp + description: products attribute 1 for stg layer + - name: col_02 + type: int + description: products attribute 2 for stg layer + billing.int_products_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for int layer + - name: col_01 + type: int + description: products attribute 1 for int layer + - name: col_02 + type: timestamp + description: products attribute 2 for int layer + billing.mart_products_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for mart layer + - name: col_02 + type: date + description: products attribute 2 for mart layer + billing.raw_prices_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: prices attribute 2 for raw layer + billing.stg_prices_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for stg layer + - name: col_01 + type: int + description: prices attribute 1 for stg layer + - name: col_02 + type: varchar + description: prices attribute 2 for stg layer + billing.int_prices_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for int layer + - name: col_01 + type: date + description: prices attribute 1 for int layer + - name: col_02 + type: timestamp + description: prices attribute 2 for int layer + billing.mart_prices_00: + owner_team: billing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for mart layer + - name: col_02 + type: timestamp + description: prices attribute 2 for mart layer + billing.raw_queries_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for raw layer + - name: col_01 + type: timestamp + description: queries attribute 1 for raw layer + - name: col_02 + type: timestamp + description: queries attribute 2 for raw layer + billing.stg_queries_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: int + description: queries attribute 1 for stg layer + - name: col_02 + type: varchar + description: queries attribute 2 for stg layer + billing.int_queries_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for int layer + - name: col_01 + type: timestamp + description: queries attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for int layer + billing.mart_queries_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: queries attribute 0 for mart layer + - name: col_01 + type: varchar + description: queries attribute 1 for mart layer + - name: col_02 + type: timestamp + description: queries attribute 2 for mart layer + billing.raw_clicks_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for raw layer + - name: col_01 + type: varchar + description: clicks attribute 1 for raw layer + - name: col_02 + type: varchar + description: clicks attribute 2 for raw layer + billing.stg_clicks_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for stg layer + - name: col_01 + type: date + description: clicks attribute 1 for stg layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for stg layer + billing.int_clicks_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for int layer + - name: col_01 + type: date + description: clicks attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for int layer + billing.mart_clicks_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for mart layer + - name: col_01 + type: int + description: clicks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for mart layer + billing.raw_payouts_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for raw layer + - name: col_01 + type: int + description: payouts attribute 1 for raw layer + - name: col_02 + type: date + description: payouts attribute 2 for raw layer + billing.stg_payouts_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for stg layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + billing.int_payouts_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for int layer + - name: col_01 + type: date + description: payouts attribute 1 for int layer + - name: col_02 + type: timestamp + description: payouts attribute 2 for int layer + billing.mart_payouts_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for mart layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for mart layer + - name: col_02 + type: int + description: payouts attribute 2 for mart layer + billing.raw_settlements_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for raw layer + - name: col_01 + type: int + description: settlements attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for raw layer + billing.stg_settlements_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for stg layer + - name: col_01 + type: int + description: settlements attribute 1 for stg layer + - name: col_02 + type: date + description: settlements attribute 2 for stg layer + billing.int_settlements_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for int layer + - name: col_01 + type: int + description: settlements attribute 1 for int layer + - name: col_02 + type: date + description: settlements attribute 2 for int layer + billing.mart_settlements_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for mart layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for mart layer + billing.raw_tickets_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for raw layer + - name: col_01 + type: varchar + description: tickets attribute 1 for raw layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for raw layer + billing.stg_tickets_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: tickets attribute 0 for stg layer + - name: col_01 + type: date + description: tickets attribute 1 for stg layer + - name: col_02 + type: varchar + description: tickets attribute 2 for stg layer + billing.int_tickets_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for int layer + - name: col_01 + type: varchar + description: tickets attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for int layer + billing.mart_tickets_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for mart layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + billing.raw_csat_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for raw layer + - name: col_01 + type: timestamp + description: csat attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for raw layer + billing.stg_csat_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for stg layer + - name: col_02 + type: varchar + description: csat attribute 2 for stg layer + billing.int_csat_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: csat attribute 0 for int layer + - name: col_01 + type: int + description: csat attribute 1 for int layer + - name: col_02 + type: timestamp + description: csat attribute 2 for int layer + billing.mart_csat_00: + owner_team: billing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: timestamp + description: csat attribute 2 for mart layer + billing.raw_headcount_00: + owner_team: billing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: headcount attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for raw layer + - name: col_02 + type: varchar + description: headcount attribute 2 for raw layer + billing.stg_headcount_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: headcount attribute 0 for stg layer + - name: col_01 + type: date + description: headcount attribute 1 for stg layer + - name: col_02 + type: varchar + description: headcount attribute 2 for stg layer + billing.int_headcount_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: varchar + description: headcount attribute 2 for int layer + billing.mart_headcount_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for mart layer + - name: col_01 + type: date + description: headcount attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for mart layer + billing.raw_payroll_00: + owner_team: billing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: payroll attribute 0 for raw layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for raw layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for raw layer + billing.stg_payroll_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for stg layer + - name: col_01 + type: varchar + description: payroll attribute 1 for stg layer + - name: col_02 + type: varchar + description: payroll attribute 2 for stg layer + billing.int_payroll_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: date + description: payroll attribute 1 for int layer + - name: col_02 + type: varchar + description: payroll attribute 2 for int layer + billing.mart_payroll_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for mart layer + - name: col_01 + type: int + description: payroll attribute 1 for mart layer + - name: col_02 + type: date + description: payroll attribute 2 for mart layer + billing.raw_forecast_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for raw layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + billing.stg_forecast_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for stg layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for stg layer + - name: col_02 + type: int + description: forecast attribute 2 for stg layer + billing.int_forecast_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for int layer + - name: col_02 + type: varchar + description: forecast attribute 2 for int layer + billing.mart_forecast_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for mart layer + billing.raw_returns_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: timestamp + description: returns attribute 2 for raw layer + billing.stg_returns_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for stg layer + - name: col_01 + type: varchar + description: returns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: returns attribute 2 for stg layer + billing.int_returns_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for int layer + - name: col_01 + type: varchar + description: returns attribute 1 for int layer + - name: col_02 + type: timestamp + description: returns attribute 2 for int layer + billing.mart_returns_00: + owner_team: billing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: returns attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for mart layer + - name: col_02 + type: date + description: returns attribute 2 for mart layer + billing.raw_suppliers_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for raw layer + billing.stg_suppliers_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: suppliers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for stg layer + - name: col_02 + type: date + description: suppliers attribute 2 for stg layer + billing.int_suppliers_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for int layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for int layer + billing.mart_suppliers_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: date + description: suppliers attribute 1 for mart layer + - name: col_02 + type: int + description: suppliers attribute 2 for mart layer + billing.raw_warehouses_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for raw layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for raw layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for raw layer + billing.stg_warehouses_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for stg layer + - name: col_01 + type: date + description: warehouses attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for stg layer + billing.int_warehouses_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for int layer + - name: col_01 + type: int + description: warehouses attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for int layer + billing.mart_warehouses_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for mart layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for mart layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for mart layer + billing.raw_promotions_00: + owner_team: billing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for raw layer + - name: col_01 + type: varchar + description: promotions attribute 1 for raw layer + - name: col_02 + type: int + description: promotions attribute 2 for raw layer + billing.stg_promotions_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for stg layer + - name: col_01 + type: int + description: promotions attribute 1 for stg layer + - name: col_02 + type: date + description: promotions attribute 2 for stg layer + billing.int_promotions_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for int layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for int layer + billing.mart_promotions_00: + owner_team: billing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for mart layer + - name: col_01 + type: date + description: promotions attribute 1 for mart layer + - name: col_02 + type: date + description: promotions attribute 2 for mart layer + billing.raw_attribution_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for raw layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for raw layer + billing.stg_attribution_00: + owner_team: billing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for stg layer + - name: col_01 + type: varchar + description: attribution attribute 1 for stg layer + - name: col_02 + type: varchar + description: attribution attribute 2 for stg layer + billing.int_attribution_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for int layer + - name: col_01 + type: varchar + description: attribution attribute 1 for int layer + - name: col_02 + type: varchar + description: attribution attribute 2 for int layer + billing.mart_attribution_00: + owner_team: billing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for mart layer + - name: col_02 + type: varchar + description: attribution attribute 2 for mart layer + billing.raw_cohorts_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for raw layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for raw layer + - name: col_02 + type: date + description: cohorts attribute 2 for raw layer + billing.stg_cohorts_00: + owner_team: billing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for stg layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for stg layer + - name: col_02 + type: date + description: cohorts attribute 2 for stg layer + billing.int_cohorts_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for int layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for int layer + billing.mart_cohorts_00: + owner_team: billing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for mart layer + - name: col_01 + type: date + description: cohorts attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for mart layer + finance.raw_orders_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for raw layer + - name: col_01 + type: varchar + description: orders attribute 1 for raw layer + - name: col_02 + type: timestamp + description: orders attribute 2 for raw layer + finance.stg_orders_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for stg layer + - name: col_01 + type: int + description: orders attribute 1 for stg layer + - name: col_02 + type: varchar + description: orders attribute 2 for stg layer + finance.int_orders_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for int layer + - name: col_01 + type: int + description: orders attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for int layer + finance.mart_orders_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for mart layer + - name: col_01 + type: int + description: orders attribute 1 for mart layer + - name: col_02 + type: date + description: orders attribute 2 for mart layer + finance.raw_invoices_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for raw layer + - name: col_01 + type: date + description: invoices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for raw layer + finance.stg_invoices_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: int + description: invoices attribute 1 for stg layer + - name: col_02 + type: int + description: invoices attribute 2 for stg layer + finance.int_invoices_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for int layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for int layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for int layer + finance.mart_invoices_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for mart layer + - name: col_01 + type: varchar + description: invoices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for mart layer + finance.raw_ledger_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for raw layer + - name: col_01 + type: date + description: ledger attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for raw layer + finance.stg_ledger_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for stg layer + - name: col_01 + type: int + description: ledger attribute 1 for stg layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for stg layer + finance.int_ledger_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for int layer + - name: col_01 + type: date + description: ledger attribute 1 for int layer + - name: col_02 + type: varchar + description: ledger attribute 2 for int layer + finance.mart_ledger_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for mart layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for mart layer + finance.raw_refunds_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for raw layer + - name: col_01 + type: date + description: refunds attribute 1 for raw layer + - name: col_02 + type: date + description: refunds attribute 2 for raw layer + finance.stg_refunds_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for stg layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for stg layer + - name: col_02 + type: date + description: refunds attribute 2 for stg layer + finance.int_refunds_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for int layer + - name: col_01 + type: int + description: refunds attribute 1 for int layer + - name: col_02 + type: int + description: refunds attribute 2 for int layer + finance.mart_refunds_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for mart layer + - name: col_02 + type: varchar + description: refunds attribute 2 for mart layer + finance.raw_sessions_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: varchar + description: sessions attribute 2 for raw layer + finance.stg_sessions_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for stg layer + - name: col_01 + type: date + description: sessions attribute 1 for stg layer + - name: col_02 + type: date + description: sessions attribute 2 for stg layer + finance.int_sessions_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for int layer + - name: col_01 + type: varchar + description: sessions attribute 1 for int layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for int layer + finance.mart_sessions_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: int + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + finance.raw_signups_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for raw layer + - name: col_01 + type: int + description: signups attribute 1 for raw layer + - name: col_02 + type: timestamp + description: signups attribute 2 for raw layer + finance.stg_signups_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: signups attribute 0 for stg layer + - name: col_01 + type: date + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + finance.int_signups_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for int layer + - name: col_01 + type: varchar + description: signups attribute 1 for int layer + - name: col_02 + type: timestamp + description: signups attribute 2 for int layer + finance.mart_signups_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for mart layer + - name: col_01 + type: date + description: signups attribute 1 for mart layer + - name: col_02 + type: int + description: signups attribute 2 for mart layer + finance.raw_campaigns_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for raw layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for raw layer + finance.stg_campaigns_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for stg layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for stg layer + finance.int_campaigns_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for int layer + - name: col_01 + type: int + description: campaigns attribute 1 for int layer + - name: col_02 + type: date + description: campaigns attribute 2 for int layer + finance.mart_campaigns_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for mart layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for mart layer + - name: col_02 + type: int + description: campaigns attribute 2 for mart layer + finance.raw_spend_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for raw layer + - name: col_01 + type: int + description: spend attribute 1 for raw layer + - name: col_02 + type: timestamp + description: spend attribute 2 for raw layer + finance.stg_spend_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for stg layer + - name: col_01 + type: int + description: spend attribute 1 for stg layer + - name: col_02 + type: int + description: spend attribute 2 for stg layer + finance.int_spend_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: varchar + description: spend attribute 1 for int layer + - name: col_02 + type: timestamp + description: spend attribute 2 for int layer + finance.mart_spend_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for mart layer + - name: col_01 + type: date + description: spend attribute 1 for mart layer + - name: col_02 + type: varchar + description: spend attribute 2 for mart layer + finance.raw_fraud_scores_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for raw layer + finance.stg_fraud_scores_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for stg layer + finance.int_fraud_scores_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for int layer + finance.mart_fraud_scores_00: + owner_team: finance + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for mart layer + finance.raw_chargebacks_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: date + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for raw layer + finance.stg_chargebacks_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for stg layer + finance.int_chargebacks_00: + owner_team: finance + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for int layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for int layer + - name: col_02 + type: int + description: chargebacks attribute 2 for int layer + finance.mart_chargebacks_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: int + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: int + description: chargebacks attribute 2 for mart layer + finance.raw_inventory_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for raw layer + - name: col_01 + type: date + description: inventory attribute 1 for raw layer + - name: col_02 + type: int + description: inventory attribute 2 for raw layer + finance.stg_inventory_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for stg layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for stg layer + finance.int_inventory_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for int layer + - name: col_02 + type: date + description: inventory attribute 2 for int layer + finance.mart_inventory_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for mart layer + - name: col_01 + type: date + description: inventory attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for mart layer + finance.raw_shipments_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for raw layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for raw layer + - name: col_02 + type: date + description: shipments attribute 2 for raw layer + finance.stg_shipments_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + finance.int_shipments_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for int layer + - name: col_01 + type: varchar + description: shipments attribute 1 for int layer + - name: col_02 + type: int + description: shipments attribute 2 for int layer + finance.mart_shipments_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for mart layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for mart layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for mart layer + finance.raw_carriers_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for raw layer + - name: col_01 + type: varchar + description: carriers attribute 1 for raw layer + - name: col_02 + type: date + description: carriers attribute 2 for raw layer + finance.stg_carriers_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: carriers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for stg layer + - name: col_02 + type: varchar + description: carriers attribute 2 for stg layer + finance.int_carriers_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: carriers attribute 0 for int layer + - name: col_01 + type: date + description: carriers attribute 1 for int layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for int layer + finance.mart_carriers_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for mart layer + - name: col_01 + type: varchar + description: carriers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for mart layer + finance.raw_products_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for raw layer + - name: col_02 + type: date + description: products attribute 2 for raw layer + finance.stg_products_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for stg layer + - name: col_01 + type: date + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + finance.int_products_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for int layer + - name: col_01 + type: varchar + description: products attribute 1 for int layer + - name: col_02 + type: int + description: products attribute 2 for int layer + finance.mart_products_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for mart layer + - name: col_01 + type: varchar + description: products attribute 1 for mart layer + - name: col_02 + type: int + description: products attribute 2 for mart layer + finance.raw_prices_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: prices attribute 2 for raw layer + finance.stg_prices_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for stg layer + - name: col_01 + type: date + description: prices attribute 1 for stg layer + - name: col_02 + type: int + description: prices attribute 2 for stg layer + finance.int_prices_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for int layer + - name: col_01 + type: timestamp + description: prices attribute 1 for int layer + - name: col_02 + type: timestamp + description: prices attribute 2 for int layer + finance.mart_prices_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for mart layer + - name: col_01 + type: date + description: prices attribute 1 for mart layer + - name: col_02 + type: int + description: prices attribute 2 for mart layer + finance.raw_queries_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for raw layer + - name: col_02 + type: timestamp + description: queries attribute 2 for raw layer + finance.stg_queries_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: int + description: queries attribute 1 for stg layer + - name: col_02 + type: timestamp + description: queries attribute 2 for stg layer + finance.int_queries_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for int layer + - name: col_01 + type: timestamp + description: queries attribute 1 for int layer + - name: col_02 + type: date + description: queries attribute 2 for int layer + finance.mart_queries_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for mart layer + - name: col_01 + type: date + description: queries attribute 1 for mart layer + - name: col_02 + type: varchar + description: queries attribute 2 for mart layer + finance.raw_clicks_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for raw layer + - name: col_01 + type: date + description: clicks attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for raw layer + finance.stg_clicks_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: date + description: clicks attribute 2 for stg layer + finance.int_clicks_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for int layer + - name: col_01 + type: timestamp + description: clicks attribute 1 for int layer + - name: col_02 + type: varchar + description: clicks attribute 2 for int layer + finance.mart_clicks_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for mart layer + - name: col_01 + type: int + description: clicks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for mart layer + finance.raw_payouts_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for raw layer + - name: col_01 + type: int + description: payouts attribute 1 for raw layer + - name: col_02 + type: date + description: payouts attribute 2 for raw layer + finance.stg_payouts_00: + owner_team: finance + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: date + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + finance.int_payouts_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for int layer + - name: col_01 + type: varchar + description: payouts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for int layer + finance.mart_payouts_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + finance.raw_settlements_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for raw layer + - name: col_01 + type: int + description: settlements attribute 1 for raw layer + - name: col_02 + type: date + description: settlements attribute 2 for raw layer + finance.stg_settlements_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for stg layer + - name: col_01 + type: date + description: settlements attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for stg layer + finance.int_settlements_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for int layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for int layer + - name: col_02 + type: varchar + description: settlements attribute 2 for int layer + finance.mart_settlements_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for mart layer + - name: col_01 + type: int + description: settlements attribute 1 for mart layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for mart layer + finance.raw_tickets_00: + owner_team: finance + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for raw layer + - name: col_01 + type: int + description: tickets attribute 1 for raw layer + - name: col_02 + type: date + description: tickets attribute 2 for raw layer + finance.stg_tickets_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for stg layer + - name: col_01 + type: varchar + description: tickets attribute 1 for stg layer + - name: col_02 + type: date + description: tickets attribute 2 for stg layer + finance.int_tickets_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for int layer + - name: col_01 + type: date + description: tickets attribute 1 for int layer + - name: col_02 + type: varchar + description: tickets attribute 2 for int layer + finance.mart_tickets_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: tickets attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for mart layer + finance.raw_csat_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for raw layer + - name: col_01 + type: timestamp + description: csat attribute 1 for raw layer + - name: col_02 + type: int + description: csat attribute 2 for raw layer + finance.stg_csat_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for stg layer + - name: col_01 + type: int + description: csat attribute 1 for stg layer + - name: col_02 + type: date + description: csat attribute 2 for stg layer + finance.int_csat_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for int layer + - name: col_01 + type: timestamp + description: csat attribute 1 for int layer + - name: col_02 + type: varchar + description: csat attribute 2 for int layer + finance.mart_csat_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: varchar + description: csat attribute 2 for mart layer + finance.raw_headcount_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: headcount attribute 0 for raw layer + - name: col_01 + type: varchar + description: headcount attribute 1 for raw layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for raw layer + finance.stg_headcount_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for stg layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for stg layer + finance.int_headcount_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for int layer + - name: col_01 + type: date + description: headcount attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for int layer + finance.mart_headcount_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for mart layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for mart layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for mart layer + finance.raw_payroll_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for raw layer + - name: col_01 + type: date + description: payroll attribute 1 for raw layer + - name: col_02 + type: varchar + description: payroll attribute 2 for raw layer + finance.stg_payroll_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for stg layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for stg layer + finance.int_payroll_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: varchar + description: payroll attribute 1 for int layer + - name: col_02 + type: varchar + description: payroll attribute 2 for int layer + finance.mart_payroll_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for mart layer + - name: col_01 + type: date + description: payroll attribute 1 for mart layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for mart layer + finance.raw_forecast_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for raw layer + - name: col_01 + type: date + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + finance.stg_forecast_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for stg layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for stg layer + finance.int_forecast_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for int layer + - name: col_01 + type: date + description: forecast attribute 1 for int layer + - name: col_02 + type: int + description: forecast attribute 2 for int layer + finance.mart_forecast_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for mart layer + - name: col_01 + type: date + description: forecast attribute 1 for mart layer + - name: col_02 + type: varchar + description: forecast attribute 2 for mart layer + finance.raw_returns_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for raw layer + - name: col_01 + type: timestamp + description: returns attribute 1 for raw layer + - name: col_02 + type: date + description: returns attribute 2 for raw layer + finance.stg_returns_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: returns attribute 1 for stg layer + - name: col_02 + type: int + description: returns attribute 2 for stg layer + finance.int_returns_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: returns attribute 0 for int layer + - name: col_01 + type: varchar + description: returns attribute 1 for int layer + - name: col_02 + type: date + description: returns attribute 2 for int layer + finance.mart_returns_00: + owner_team: finance + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for mart layer + - name: col_01 + type: int + description: returns attribute 1 for mart layer + - name: col_02 + type: int + description: returns attribute 2 for mart layer + finance.raw_suppliers_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for raw layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for raw layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for raw layer + finance.stg_suppliers_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for stg layer + - name: col_01 + type: int + description: suppliers attribute 1 for stg layer + - name: col_02 + type: int + description: suppliers attribute 2 for stg layer + finance.int_suppliers_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for int layer + - name: col_01 + type: date + description: suppliers attribute 1 for int layer + - name: col_02 + type: int + description: suppliers attribute 2 for int layer + finance.mart_suppliers_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for mart layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for mart layer + finance.raw_warehouses_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for raw layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for raw layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for raw layer + finance.stg_warehouses_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for stg layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for stg layer + finance.int_warehouses_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for int layer + - name: col_01 + type: date + description: warehouses attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for int layer + finance.mart_warehouses_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for mart layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for mart layer + finance.raw_promotions_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for raw layer + - name: col_01 + type: date + description: promotions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for raw layer + finance.stg_promotions_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for stg layer + finance.int_promotions_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for int layer + - name: col_01 + type: int + description: promotions attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for int layer + finance.mart_promotions_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for mart layer + - name: col_01 + type: varchar + description: promotions attribute 1 for mart layer + - name: col_02 + type: varchar + description: promotions attribute 2 for mart layer + finance.raw_attribution_00: + owner_team: finance + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: attribution attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for raw layer + - name: col_02 + type: int + description: attribution attribute 2 for raw layer + finance.stg_attribution_00: + owner_team: finance + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for stg layer + - name: col_01 + type: int + description: attribution attribute 1 for stg layer + - name: col_02 + type: int + description: attribution attribute 2 for stg layer + finance.int_attribution_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for int layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for int layer + - name: col_02 + type: varchar + description: attribution attribute 2 for int layer + finance.mart_attribution_00: + owner_team: finance + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: date + description: attribution attribute 2 for mart layer + finance.raw_cohorts_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for raw layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for raw layer + - name: col_02 + type: date + description: cohorts attribute 2 for raw layer + finance.stg_cohorts_00: + owner_team: finance + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for stg layer + - name: col_01 + type: date + description: cohorts attribute 1 for stg layer + - name: col_02 + type: int + description: cohorts attribute 2 for stg layer + finance.int_cohorts_00: + owner_team: finance + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for int layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for int layer + - name: col_02 + type: int + description: cohorts attribute 2 for int layer + finance.mart_cohorts_00: + owner_team: finance + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for mart layer + - name: col_02 + type: int + description: cohorts attribute 2 for mart layer + growth.raw_orders_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: orders attribute 0 for raw layer + - name: col_01 + type: timestamp + description: orders attribute 1 for raw layer + - name: col_02 + type: timestamp + description: orders attribute 2 for raw layer + growth.stg_orders_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: date + description: orders attribute 1 for stg layer + - name: col_02 + type: int + description: orders attribute 2 for stg layer + growth.int_orders_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for int layer + - name: col_02 + type: date + description: orders attribute 2 for int layer + growth.mart_orders_00: + owner_team: growth + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: orders attribute 0 for mart layer + - name: col_01 + type: int + description: orders attribute 1 for mart layer + - name: col_02 + type: timestamp + description: orders attribute 2 for mart layer + growth.raw_invoices_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for raw layer + - name: col_01 + type: int + description: invoices attribute 1 for raw layer + - name: col_02 + type: int + description: invoices attribute 2 for raw layer + growth.stg_invoices_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for stg layer + - name: col_02 + type: date + description: invoices attribute 2 for stg layer + growth.int_invoices_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for int layer + - name: col_01 + type: date + description: invoices attribute 1 for int layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for int layer + growth.mart_invoices_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for mart layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for mart layer + - name: col_02 + type: varchar + description: invoices attribute 2 for mart layer + growth.raw_ledger_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for raw layer + - name: col_01 + type: varchar + description: ledger attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for raw layer + growth.stg_ledger_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for stg layer + - name: col_01 + type: int + description: ledger attribute 1 for stg layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for stg layer + growth.int_ledger_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for int layer + - name: col_01 + type: varchar + description: ledger attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for int layer + growth.mart_ledger_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for mart layer + - name: col_01 + type: date + description: ledger attribute 1 for mart layer + - name: col_02 + type: varchar + description: ledger attribute 2 for mart layer + growth.raw_refunds_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for raw layer + - name: col_01 + type: int + description: refunds attribute 1 for raw layer + - name: col_02 + type: date + description: refunds attribute 2 for raw layer + growth.stg_refunds_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for stg layer + - name: col_02 + type: int + description: refunds attribute 2 for stg layer + growth.int_refunds_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for int layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for int layer + growth.mart_refunds_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for mart layer + growth.raw_sessions_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for raw layer + growth.stg_sessions_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for stg layer + - name: col_01 + type: int + description: sessions attribute 1 for stg layer + - name: col_02 + type: int + description: sessions attribute 2 for stg layer + growth.int_sessions_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for int layer + - name: col_02 + type: varchar + description: sessions attribute 2 for int layer + growth.mart_sessions_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for mart layer + - name: col_01 + type: varchar + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + growth.raw_signups_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for raw layer + - name: col_01 + type: timestamp + description: signups attribute 1 for raw layer + - name: col_02 + type: int + description: signups attribute 2 for raw layer + growth.stg_signups_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for stg layer + - name: col_01 + type: date + description: signups attribute 1 for stg layer + - name: col_02 + type: timestamp + description: signups attribute 2 for stg layer + growth.int_signups_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for int layer + - name: col_01 + type: int + description: signups attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for int layer + growth.mart_signups_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for mart layer + - name: col_02 + type: int + description: signups attribute 2 for mart layer + growth.raw_campaigns_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for raw layer + - name: col_02 + type: date + description: campaigns attribute 2 for raw layer + growth.stg_campaigns_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for stg layer + growth.int_campaigns_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for int layer + - name: col_01 + type: date + description: campaigns attribute 1 for int layer + - name: col_02 + type: date + description: campaigns attribute 2 for int layer + growth.mart_campaigns_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for mart layer + growth.raw_spend_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for raw layer + growth.stg_spend_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for stg layer + growth.int_spend_00: + owner_team: growth + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for int layer + - name: col_02 + type: varchar + description: spend attribute 2 for int layer + growth.mart_spend_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for mart layer + - name: col_01 + type: timestamp + description: spend attribute 1 for mart layer + - name: col_02 + type: varchar + description: spend attribute 2 for mart layer + growth.raw_fraud_scores_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for raw layer + growth.stg_fraud_scores_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for stg layer + growth.int_fraud_scores_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for int layer + growth.mart_fraud_scores_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for mart layer + growth.raw_chargebacks_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: int + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for raw layer + growth.stg_chargebacks_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: int + description: chargebacks attribute 2 for stg layer + growth.int_chargebacks_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for int layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for int layer + - name: col_02 + type: int + description: chargebacks attribute 2 for int layer + growth.mart_chargebacks_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: date + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: date + description: chargebacks attribute 2 for mart layer + growth.raw_inventory_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for raw layer + - name: col_01 + type: int + description: inventory attribute 1 for raw layer + - name: col_02 + type: int + description: inventory attribute 2 for raw layer + growth.stg_inventory_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for stg layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for stg layer + growth.int_inventory_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for int layer + - name: col_01 + type: date + description: inventory attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for int layer + growth.mart_inventory_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for mart layer + - name: col_01 + type: int + description: inventory attribute 1 for mart layer + - name: col_02 + type: varchar + description: inventory attribute 2 for mart layer + growth.raw_shipments_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for raw layer + - name: col_01 + type: int + description: shipments attribute 1 for raw layer + - name: col_02 + type: varchar + description: shipments attribute 2 for raw layer + growth.stg_shipments_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for stg layer + - name: col_01 + type: varchar + description: shipments attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for stg layer + growth.int_shipments_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for int layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for int layer + - name: col_02 + type: date + description: shipments attribute 2 for int layer + growth.mart_shipments_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for mart layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for mart layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for mart layer + growth.raw_carriers_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for raw layer + - name: col_01 + type: varchar + description: carriers attribute 1 for raw layer + - name: col_02 + type: varchar + description: carriers attribute 2 for raw layer + growth.stg_carriers_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for stg layer + - name: col_02 + type: varchar + description: carriers attribute 2 for stg layer + growth.int_carriers_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for int layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for int layer + growth.mart_carriers_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for mart layer + growth.raw_products_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for raw layer + - name: col_01 + type: varchar + description: products attribute 1 for raw layer + - name: col_02 + type: timestamp + description: products attribute 2 for raw layer + growth.stg_products_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for stg layer + - name: col_01 + type: varchar + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + growth.int_products_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for int layer + - name: col_02 + type: varchar + description: products attribute 2 for int layer + growth.mart_products_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: products attribute 0 for mart layer + - name: col_01 + type: varchar + description: products attribute 1 for mart layer + - name: col_02 + type: timestamp + description: products attribute 2 for mart layer + growth.raw_prices_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for raw layer + - name: col_01 + type: date + description: prices attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for raw layer + growth.stg_prices_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for stg layer + - name: col_01 + type: int + description: prices attribute 1 for stg layer + - name: col_02 + type: int + description: prices attribute 2 for stg layer + growth.int_prices_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: prices attribute 0 for int layer + - name: col_01 + type: date + description: prices attribute 1 for int layer + - name: col_02 + type: timestamp + description: prices attribute 2 for int layer + growth.mart_prices_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for mart layer + - name: col_01 + type: varchar + description: prices attribute 1 for mart layer + - name: col_02 + type: timestamp + description: prices attribute 2 for mart layer + growth.raw_queries_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for raw layer + - name: col_02 + type: date + description: queries attribute 2 for raw layer + growth.stg_queries_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: date + description: queries attribute 1 for stg layer + - name: col_02 + type: date + description: queries attribute 2 for stg layer + growth.int_queries_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for int layer + - name: col_01 + type: varchar + description: queries attribute 1 for int layer + - name: col_02 + type: timestamp + description: queries attribute 2 for int layer + growth.mart_queries_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for mart layer + - name: col_01 + type: varchar + description: queries attribute 1 for mart layer + - name: col_02 + type: date + description: queries attribute 2 for mart layer + growth.raw_clicks_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: int + description: clicks attribute 2 for raw layer + growth.stg_clicks_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for stg layer + - name: col_01 + type: date + description: clicks attribute 1 for stg layer + - name: col_02 + type: varchar + description: clicks attribute 2 for stg layer + growth.int_clicks_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for int layer + - name: col_02 + type: varchar + description: clicks attribute 2 for int layer + growth.mart_clicks_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for mart layer + - name: col_01 + type: varchar + description: clicks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for mart layer + growth.raw_payouts_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for raw layer + - name: col_01 + type: varchar + description: payouts attribute 1 for raw layer + - name: col_02 + type: timestamp + description: payouts attribute 2 for raw layer + growth.stg_payouts_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for stg layer + - name: col_01 + type: date + description: payouts attribute 1 for stg layer + - name: col_02 + type: date + description: payouts attribute 2 for stg layer + growth.int_payouts_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: payouts attribute 0 for int layer + - name: col_01 + type: int + description: payouts attribute 1 for int layer + - name: col_02 + type: int + description: payouts attribute 2 for int layer + growth.mart_payouts_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for mart layer + - name: col_01 + type: varchar + description: payouts attribute 1 for mart layer + - name: col_02 + type: varchar + description: payouts attribute 2 for mart layer + growth.raw_settlements_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for raw layer + - name: col_02 + type: varchar + description: settlements attribute 2 for raw layer + growth.stg_settlements_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for stg layer + growth.int_settlements_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for int layer + - name: col_01 + type: varchar + description: settlements attribute 1 for int layer + - name: col_02 + type: date + description: settlements attribute 2 for int layer + growth.mart_settlements_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for mart layer + - name: col_01 + type: varchar + description: settlements attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for mart layer + growth.raw_tickets_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for raw layer + - name: col_01 + type: date + description: tickets attribute 1 for raw layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for raw layer + growth.stg_tickets_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for stg layer + - name: col_02 + type: varchar + description: tickets attribute 2 for stg layer + growth.int_tickets_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: tickets attribute 0 for int layer + - name: col_01 + type: int + description: tickets attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for int layer + growth.mart_tickets_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for mart layer + - name: col_01 + type: date + description: tickets attribute 1 for mart layer + - name: col_02 + type: date + description: tickets attribute 2 for mart layer + growth.raw_csat_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for raw layer + - name: col_01 + type: date + description: csat attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for raw layer + growth.stg_csat_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for stg layer + - name: col_01 + type: varchar + description: csat attribute 1 for stg layer + - name: col_02 + type: timestamp + description: csat attribute 2 for stg layer + growth.int_csat_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: csat attribute 0 for int layer + - name: col_01 + type: int + description: csat attribute 1 for int layer + - name: col_02 + type: varchar + description: csat attribute 2 for int layer + growth.mart_csat_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for mart layer + - name: col_01 + type: date + description: csat attribute 1 for mart layer + - name: col_02 + type: int + description: csat attribute 2 for mart layer + growth.raw_headcount_00: + owner_team: growth + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for raw layer + - name: col_01 + type: int + description: headcount attribute 1 for raw layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for raw layer + growth.stg_headcount_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: headcount attribute 0 for stg layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for stg layer + - name: col_02 + type: int + description: headcount attribute 2 for stg layer + growth.int_headcount_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for int layer + growth.mart_headcount_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for mart layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for mart layer + - name: col_02 + type: int + description: headcount attribute 2 for mart layer + growth.raw_payroll_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for raw layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for raw layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for raw layer + growth.stg_payroll_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for stg layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for stg layer + - name: col_02 + type: date + description: payroll attribute 2 for stg layer + growth.int_payroll_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for int layer + - name: col_02 + type: date + description: payroll attribute 2 for int layer + growth.mart_payroll_00: + owner_team: growth + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for mart layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for mart layer + - name: col_02 + type: int + description: payroll attribute 2 for mart layer + growth.raw_forecast_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for raw layer + - name: col_01 + type: varchar + description: forecast attribute 1 for raw layer + - name: col_02 + type: varchar + description: forecast attribute 2 for raw layer + growth.stg_forecast_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for stg layer + - name: col_01 + type: int + description: forecast attribute 1 for stg layer + - name: col_02 + type: int + description: forecast attribute 2 for stg layer + growth.int_forecast_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for int layer + growth.mart_forecast_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for mart layer + - name: col_01 + type: varchar + description: forecast attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for mart layer + growth.raw_returns_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for raw layer + - name: col_01 + type: timestamp + description: returns attribute 1 for raw layer + - name: col_02 + type: int + description: returns attribute 2 for raw layer + growth.stg_returns_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for stg layer + - name: col_01 + type: date + description: returns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: returns attribute 2 for stg layer + growth.int_returns_00: + owner_team: growth + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: returns attribute 0 for int layer + - name: col_01 + type: timestamp + description: returns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for int layer + growth.mart_returns_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for mart layer + - name: col_01 + type: int + description: returns attribute 1 for mart layer + - name: col_02 + type: int + description: returns attribute 2 for mart layer + growth.raw_suppliers_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for raw layer + - name: col_01 + type: int + description: suppliers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for raw layer + growth.stg_suppliers_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for stg layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for stg layer + growth.int_suppliers_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for int layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for int layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for int layer + growth.mart_suppliers_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for mart layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for mart layer + growth.raw_warehouses_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for raw layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for raw layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for raw layer + growth.stg_warehouses_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for stg layer + - name: col_02 + type: date + description: warehouses attribute 2 for stg layer + growth.int_warehouses_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for int layer + - name: col_01 + type: date + description: warehouses attribute 1 for int layer + - name: col_02 + type: date + description: warehouses attribute 2 for int layer + growth.mart_warehouses_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for mart layer + - name: col_01 + type: date + description: warehouses attribute 1 for mart layer + - name: col_02 + type: int + description: warehouses attribute 2 for mart layer + growth.raw_promotions_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for raw layer + - name: col_01 + type: int + description: promotions attribute 1 for raw layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for raw layer + growth.stg_promotions_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + growth.int_promotions_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for int layer + - name: col_01 + type: date + description: promotions attribute 1 for int layer + - name: col_02 + type: date + description: promotions attribute 2 for int layer + growth.mart_promotions_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for mart layer + - name: col_01 + type: int + description: promotions attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for mart layer + growth.raw_attribution_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for raw layer + - name: col_02 + type: date + description: attribution attribute 2 for raw layer + growth.stg_attribution_00: + owner_team: growth + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for stg layer + - name: col_01 + type: date + description: attribution attribute 1 for stg layer + - name: col_02 + type: varchar + description: attribution attribute 2 for stg layer + growth.int_attribution_00: + owner_team: growth + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for int layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for int layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for int layer + growth.mart_attribution_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for mart layer + growth.raw_cohorts_00: + owner_team: growth + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for raw layer + - name: col_01 + type: int + description: cohorts attribute 1 for raw layer + - name: col_02 + type: int + description: cohorts attribute 2 for raw layer + growth.stg_cohorts_00: + owner_team: growth + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for stg layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for stg layer + growth.int_cohorts_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for int layer + - name: col_01 + type: date + description: cohorts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for int layer + growth.mart_cohorts_00: + owner_team: growth + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for mart layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for mart layer + marketing.raw_orders_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for raw layer + - name: col_01 + type: varchar + description: orders attribute 1 for raw layer + - name: col_02 + type: timestamp + description: orders attribute 2 for raw layer + marketing.stg_orders_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for stg layer + - name: col_02 + type: date + description: orders attribute 2 for stg layer + marketing.int_orders_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: orders attribute 0 for int layer + - name: col_01 + type: int + description: orders attribute 1 for int layer + - name: col_02 + type: timestamp + description: orders attribute 2 for int layer + marketing.mart_orders_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: orders attribute 0 for mart layer + - name: col_01 + type: int + description: orders attribute 1 for mart layer + - name: col_02 + type: date + description: orders attribute 2 for mart layer + marketing.raw_invoices_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for raw layer + - name: col_01 + type: date + description: invoices attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for raw layer + marketing.stg_invoices_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: int + description: invoices attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for stg layer + marketing.int_invoices_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for int layer + - name: col_01 + type: varchar + description: invoices attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for int layer + marketing.mart_invoices_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for mart layer + - name: col_01 + type: date + description: invoices attribute 1 for mart layer + - name: col_02 + type: int + description: invoices attribute 2 for mart layer + marketing.raw_ledger_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for raw layer + marketing.stg_ledger_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for stg layer + - name: col_01 + type: date + description: ledger attribute 1 for stg layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for stg layer + marketing.int_ledger_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for int layer + - name: col_01 + type: int + description: ledger attribute 1 for int layer + - name: col_02 + type: varchar + description: ledger attribute 2 for int layer + marketing.mart_ledger_00: + owner_team: marketing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for mart layer + marketing.raw_refunds_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for raw layer + - name: col_01 + type: int + description: refunds attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for raw layer + marketing.stg_refunds_00: + owner_team: marketing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for stg layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for stg layer + marketing.int_refunds_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for int layer + - name: col_01 + type: int + description: refunds attribute 1 for int layer + - name: col_02 + type: int + description: refunds attribute 2 for int layer + marketing.mart_refunds_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for mart layer + - name: col_02 + type: date + description: refunds attribute 2 for mart layer + marketing.raw_sessions_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for raw layer + - name: col_01 + type: int + description: sessions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for raw layer + marketing.stg_sessions_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for stg layer + - name: col_02 + type: date + description: sessions attribute 2 for stg layer + marketing.int_sessions_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for int layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for int layer + - name: col_02 + type: date + description: sessions attribute 2 for int layer + marketing.mart_sessions_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: sessions attribute 0 for mart layer + - name: col_01 + type: varchar + description: sessions attribute 1 for mart layer + - name: col_02 + type: varchar + description: sessions attribute 2 for mart layer + marketing.raw_signups_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for raw layer + - name: col_02 + type: int + description: signups attribute 2 for raw layer + marketing.stg_signups_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for stg layer + - name: col_01 + type: varchar + description: signups attribute 1 for stg layer + - name: col_02 + type: timestamp + description: signups attribute 2 for stg layer + marketing.int_signups_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for int layer + - name: col_02 + type: timestamp + description: signups attribute 2 for int layer + marketing.mart_signups_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for mart layer + - name: col_01 + type: timestamp + description: signups attribute 1 for mart layer + - name: col_02 + type: varchar + description: signups attribute 2 for mart layer + marketing.raw_campaigns_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for raw layer + marketing.stg_campaigns_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for stg layer + - name: col_01 + type: date + description: campaigns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for stg layer + marketing.int_campaigns_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for int layer + - name: col_01 + type: date + description: campaigns attribute 1 for int layer + - name: col_02 + type: date + description: campaigns attribute 2 for int layer + marketing.mart_campaigns_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for mart layer + marketing.raw_spend_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: date + description: spend attribute 1 for raw layer + - name: col_02 + type: varchar + description: spend attribute 2 for raw layer + marketing.stg_spend_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for stg layer + - name: col_01 + type: date + description: spend attribute 1 for stg layer + - name: col_02 + type: varchar + description: spend attribute 2 for stg layer + marketing.int_spend_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for int layer + - name: col_01 + type: int + description: spend attribute 1 for int layer + - name: col_02 + type: date + description: spend attribute 2 for int layer + marketing.mart_spend_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for mart layer + - name: col_02 + type: date + description: spend attribute 2 for mart layer + marketing.raw_fraud_scores_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for raw layer + marketing.stg_fraud_scores_00: + owner_team: marketing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for stg layer + marketing.int_fraud_scores_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for int layer + marketing.mart_fraud_scores_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for mart layer + marketing.raw_chargebacks_00: + owner_team: marketing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: date + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for raw layer + marketing.stg_chargebacks_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: date + description: chargebacks attribute 2 for stg layer + marketing.int_chargebacks_00: + owner_team: marketing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for int layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for int layer + - name: col_02 + type: int + description: chargebacks attribute 2 for int layer + marketing.mart_chargebacks_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for mart layer + marketing.raw_inventory_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: inventory attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for raw layer + - name: col_02 + type: date + description: inventory attribute 2 for raw layer + marketing.stg_inventory_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: inventory attribute 0 for stg layer + - name: col_01 + type: date + description: inventory attribute 1 for stg layer + - name: col_02 + type: int + description: inventory attribute 2 for stg layer + marketing.int_inventory_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for int layer + - name: col_01 + type: int + description: inventory attribute 1 for int layer + - name: col_02 + type: varchar + description: inventory attribute 2 for int layer + marketing.mart_inventory_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for mart layer + - name: col_01 + type: varchar + description: inventory attribute 1 for mart layer + - name: col_02 + type: varchar + description: inventory attribute 2 for mart layer + marketing.raw_shipments_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for raw layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for raw layer + - name: col_02 + type: int + description: shipments attribute 2 for raw layer + marketing.stg_shipments_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + marketing.int_shipments_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for int layer + - name: col_01 + type: int + description: shipments attribute 1 for int layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for int layer + marketing.mart_shipments_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for mart layer + - name: col_02 + type: varchar + description: shipments attribute 2 for mart layer + marketing.raw_carriers_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: carriers attribute 0 for raw layer + - name: col_01 + type: date + description: carriers attribute 1 for raw layer + - name: col_02 + type: int + description: carriers attribute 2 for raw layer + marketing.stg_carriers_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for stg layer + - name: col_02 + type: int + description: carriers attribute 2 for stg layer + marketing.int_carriers_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for int layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for int layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for int layer + marketing.mart_carriers_00: + owner_team: marketing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for mart layer + - name: col_01 + type: int + description: carriers attribute 1 for mart layer + - name: col_02 + type: date + description: carriers attribute 2 for mart layer + marketing.raw_products_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for raw layer + - name: col_01 + type: varchar + description: products attribute 1 for raw layer + - name: col_02 + type: timestamp + description: products attribute 2 for raw layer + marketing.stg_products_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for stg layer + - name: col_01 + type: date + description: products attribute 1 for stg layer + - name: col_02 + type: int + description: products attribute 2 for stg layer + marketing.int_products_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for int layer + - name: col_01 + type: date + description: products attribute 1 for int layer + - name: col_02 + type: int + description: products attribute 2 for int layer + marketing.mart_products_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for mart layer + - name: col_01 + type: int + description: products attribute 1 for mart layer + - name: col_02 + type: varchar + description: products attribute 2 for mart layer + marketing.raw_prices_00: + owner_team: marketing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: timestamp + description: prices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: prices attribute 2 for raw layer + marketing.stg_prices_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for stg layer + - name: col_02 + type: int + description: prices attribute 2 for stg layer + marketing.int_prices_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for int layer + - name: col_02 + type: timestamp + description: prices attribute 2 for int layer + marketing.mart_prices_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for mart layer + - name: col_01 + type: date + description: prices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for mart layer + marketing.raw_queries_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for raw layer + - name: col_01 + type: varchar + description: queries attribute 1 for raw layer + - name: col_02 + type: varchar + description: queries attribute 2 for raw layer + marketing.stg_queries_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for stg layer + - name: col_01 + type: varchar + description: queries attribute 1 for stg layer + - name: col_02 + type: date + description: queries attribute 2 for stg layer + marketing.int_queries_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for int layer + - name: col_01 + type: int + description: queries attribute 1 for int layer + - name: col_02 + type: date + description: queries attribute 2 for int layer + marketing.mart_queries_00: + owner_team: marketing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for mart layer + - name: col_01 + type: date + description: queries attribute 1 for mart layer + - name: col_02 + type: int + description: queries attribute 2 for mart layer + marketing.raw_clicks_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for raw layer + marketing.stg_clicks_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for stg layer + - name: col_01 + type: varchar + description: clicks attribute 1 for stg layer + - name: col_02 + type: int + description: clicks attribute 2 for stg layer + marketing.int_clicks_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for int layer + - name: col_01 + type: date + description: clicks attribute 1 for int layer + - name: col_02 + type: varchar + description: clicks attribute 2 for int layer + marketing.mart_clicks_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for mart layer + - name: col_01 + type: varchar + description: clicks attribute 1 for mart layer + - name: col_02 + type: int + description: clicks attribute 2 for mart layer + marketing.raw_payouts_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for raw layer + - name: col_01 + type: date + description: payouts attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for raw layer + marketing.stg_payouts_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for stg layer + - name: col_02 + type: date + description: payouts attribute 2 for stg layer + marketing.int_payouts_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for int layer + - name: col_01 + type: varchar + description: payouts attribute 1 for int layer + - name: col_02 + type: int + description: payouts attribute 2 for int layer + marketing.mart_payouts_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: payouts attribute 0 for mart layer + - name: col_01 + type: int + description: payouts attribute 1 for mart layer + - name: col_02 + type: varchar + description: payouts attribute 2 for mart layer + marketing.raw_settlements_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for raw layer + marketing.stg_settlements_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for stg layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for stg layer + - name: col_02 + type: int + description: settlements attribute 2 for stg layer + marketing.int_settlements_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for int layer + - name: col_01 + type: varchar + description: settlements attribute 1 for int layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for int layer + marketing.mart_settlements_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for mart layer + - name: col_01 + type: varchar + description: settlements attribute 1 for mart layer + - name: col_02 + type: date + description: settlements attribute 2 for mart layer + marketing.raw_tickets_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for raw layer + - name: col_01 + type: varchar + description: tickets attribute 1 for raw layer + - name: col_02 + type: varchar + description: tickets attribute 2 for raw layer + marketing.stg_tickets_00: + owner_team: marketing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for stg layer + - name: col_01 + type: varchar + description: tickets attribute 1 for stg layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for stg layer + marketing.int_tickets_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for int layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for int layer + marketing.mart_tickets_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for mart layer + - name: col_01 + type: date + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + marketing.raw_csat_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for raw layer + - name: col_01 + type: timestamp + description: csat attribute 1 for raw layer + - name: col_02 + type: varchar + description: csat attribute 2 for raw layer + marketing.stg_csat_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for stg layer + - name: col_01 + type: int + description: csat attribute 1 for stg layer + - name: col_02 + type: timestamp + description: csat attribute 2 for stg layer + marketing.int_csat_00: + owner_team: marketing + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for int layer + - name: col_02 + type: int + description: csat attribute 2 for int layer + marketing.mart_csat_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for mart layer + - name: col_01 + type: int + description: csat attribute 1 for mart layer + - name: col_02 + type: int + description: csat attribute 2 for mart layer + marketing.raw_headcount_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for raw layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for raw layer + marketing.stg_headcount_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for stg layer + - name: col_01 + type: int + description: headcount attribute 1 for stg layer + - name: col_02 + type: int + description: headcount attribute 2 for stg layer + marketing.int_headcount_00: + owner_team: marketing + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: headcount attribute 0 for int layer + - name: col_01 + type: varchar + description: headcount attribute 1 for int layer + - name: col_02 + type: int + description: headcount attribute 2 for int layer + marketing.mart_headcount_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: headcount attribute 0 for mart layer + - name: col_01 + type: date + description: headcount attribute 1 for mart layer + - name: col_02 + type: int + description: headcount attribute 2 for mart layer + marketing.raw_payroll_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for raw layer + - name: col_02 + type: int + description: payroll attribute 2 for raw layer + marketing.stg_payroll_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for stg layer + - name: col_01 + type: int + description: payroll attribute 1 for stg layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for stg layer + marketing.int_payroll_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for int layer + - name: col_01 + type: varchar + description: payroll attribute 1 for int layer + - name: col_02 + type: int + description: payroll attribute 2 for int layer + marketing.mart_payroll_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for mart layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for mart layer + - name: col_02 + type: varchar + description: payroll attribute 2 for mart layer + marketing.raw_forecast_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for raw layer + - name: col_02 + type: int + description: forecast attribute 2 for raw layer + marketing.stg_forecast_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for stg layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for stg layer + - name: col_02 + type: int + description: forecast attribute 2 for stg layer + marketing.int_forecast_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for int layer + - name: col_01 + type: varchar + description: forecast attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for int layer + marketing.mart_forecast_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for mart layer + - name: col_01 + type: date + description: forecast attribute 1 for mart layer + - name: col_02 + type: timestamp + description: forecast attribute 2 for mart layer + marketing.raw_returns_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: date + description: returns attribute 2 for raw layer + marketing.stg_returns_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for stg layer + - name: col_02 + type: date + description: returns attribute 2 for stg layer + marketing.int_returns_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for int layer + - name: col_02 + type: timestamp + description: returns attribute 2 for int layer + marketing.mart_returns_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for mart layer + - name: col_01 + type: varchar + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + marketing.raw_suppliers_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for raw layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for raw layer + marketing.stg_suppliers_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for stg layer + marketing.int_suppliers_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for int layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for int layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for int layer + marketing.mart_suppliers_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for mart layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for mart layer + marketing.raw_warehouses_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for raw layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for raw layer + - name: col_02 + type: int + description: warehouses attribute 2 for raw layer + marketing.stg_warehouses_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for stg layer + marketing.int_warehouses_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for int layer + - name: col_01 + type: int + description: warehouses attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for int layer + marketing.mart_warehouses_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for mart layer + - name: col_02 + type: int + description: warehouses attribute 2 for mart layer + marketing.raw_promotions_00: + owner_team: marketing + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for raw layer + - name: col_01 + type: date + description: promotions attribute 1 for raw layer + - name: col_02 + type: date + description: promotions attribute 2 for raw layer + marketing.stg_promotions_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + marketing.int_promotions_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for int layer + - name: col_02 + type: date + description: promotions attribute 2 for int layer + marketing.mart_promotions_00: + owner_team: marketing + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for mart layer + marketing.raw_attribution_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for raw layer + - name: col_01 + type: int + description: attribution attribute 1 for raw layer + - name: col_02 + type: int + description: attribution attribute 2 for raw layer + marketing.stg_attribution_00: + owner_team: marketing + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: attribution attribute 0 for stg layer + - name: col_01 + type: varchar + description: attribution attribute 1 for stg layer + - name: col_02 + type: int + description: attribution attribute 2 for stg layer + marketing.int_attribution_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for int layer + - name: col_01 + type: varchar + description: attribution attribute 1 for int layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for int layer + marketing.mart_attribution_00: + owner_team: marketing + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for mart layer + marketing.raw_cohorts_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for raw layer + - name: col_01 + type: int + description: cohorts attribute 1 for raw layer + - name: col_02 + type: date + description: cohorts attribute 2 for raw layer + marketing.stg_cohorts_00: + owner_team: marketing + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for stg layer + - name: col_01 + type: date + description: cohorts attribute 1 for stg layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for stg layer + marketing.int_cohorts_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for int layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for int layer + - name: col_02 + type: int + description: cohorts attribute 2 for int layer + marketing.mart_cohorts_00: + owner_team: marketing + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for mart layer + - name: col_01 + type: date + description: cohorts attribute 1 for mart layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for mart layer + risk.raw_orders_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for raw layer + - name: col_01 + type: timestamp + description: orders attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for raw layer + risk.stg_orders_00: + owner_team: risk + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for stg layer + - name: col_02 + type: date + description: orders attribute 2 for stg layer + risk.int_orders_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for int layer + - name: col_01 + type: int + description: orders attribute 1 for int layer + - name: col_02 + type: timestamp + description: orders attribute 2 for int layer + risk.mart_orders_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for mart layer + - name: col_02 + type: date + description: orders attribute 2 for mart layer + risk.raw_invoices_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for raw layer + - name: col_01 + type: date + description: invoices attribute 1 for raw layer + - name: col_02 + type: int + description: invoices attribute 2 for raw layer + risk.stg_invoices_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for stg layer + - name: col_02 + type: int + description: invoices attribute 2 for stg layer + risk.int_invoices_00: + owner_team: risk + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for int layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for int layer + risk.mart_invoices_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for mart layer + - name: col_01 + type: date + description: invoices attribute 1 for mart layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for mart layer + risk.raw_ledger_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for raw layer + - name: col_01 + type: date + description: ledger attribute 1 for raw layer + - name: col_02 + type: varchar + description: ledger attribute 2 for raw layer + risk.stg_ledger_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for stg layer + - name: col_02 + type: int + description: ledger attribute 2 for stg layer + risk.int_ledger_00: + owner_team: risk + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for int layer + - name: col_01 + type: int + description: ledger attribute 1 for int layer + - name: col_02 + type: date + description: ledger attribute 2 for int layer + risk.mart_ledger_00: + owner_team: risk + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for mart layer + - name: col_01 + type: int + description: ledger attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for mart layer + risk.raw_refunds_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for raw layer + - name: col_01 + type: varchar + description: refunds attribute 1 for raw layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for raw layer + risk.stg_refunds_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for stg layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for stg layer + - name: col_02 + type: date + description: refunds attribute 2 for stg layer + risk.int_refunds_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for int layer + risk.mart_refunds_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for mart layer + - name: col_01 + type: int + description: refunds attribute 1 for mart layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for mart layer + risk.raw_sessions_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: date + description: sessions attribute 2 for raw layer + risk.stg_sessions_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for stg layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for stg layer + - name: col_02 + type: int + description: sessions attribute 2 for stg layer + risk.int_sessions_00: + owner_team: risk + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for int layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for int layer + - name: col_02 + type: varchar + description: sessions attribute 2 for int layer + risk.mart_sessions_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for mart layer + - name: col_01 + type: date + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + risk.raw_signups_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for raw layer + - name: col_01 + type: int + description: signups attribute 1 for raw layer + - name: col_02 + type: date + description: signups attribute 2 for raw layer + risk.stg_signups_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for stg layer + - name: col_01 + type: date + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + risk.int_signups_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for int layer + - name: col_01 + type: date + description: signups attribute 1 for int layer + - name: col_02 + type: varchar + description: signups attribute 2 for int layer + risk.mart_signups_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: signups attribute 0 for mart layer + - name: col_01 + type: int + description: signups attribute 1 for mart layer + - name: col_02 + type: timestamp + description: signups attribute 2 for mart layer + risk.raw_campaigns_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + risk.stg_campaigns_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for stg layer + risk.int_campaigns_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for int layer + risk.mart_campaigns_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for mart layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for mart layer + risk.raw_spend_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: varchar + description: spend attribute 1 for raw layer + - name: col_02 + type: timestamp + description: spend attribute 2 for raw layer + risk.stg_spend_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for stg layer + - name: col_02 + type: int + description: spend attribute 2 for stg layer + risk.int_spend_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: timestamp + description: spend attribute 1 for int layer + - name: col_02 + type: timestamp + description: spend attribute 2 for int layer + risk.mart_spend_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: spend attribute 0 for mart layer + - name: col_01 + type: int + description: spend attribute 1 for mart layer + - name: col_02 + type: varchar + description: spend attribute 2 for mart layer + risk.raw_fraud_scores_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for raw layer + risk.stg_fraud_scores_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for stg layer + risk.int_fraud_scores_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for int layer + risk.mart_fraud_scores_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for mart layer + risk.raw_chargebacks_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for raw layer + risk.stg_chargebacks_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: date + description: chargebacks attribute 2 for stg layer + risk.int_chargebacks_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for int layer + - name: col_01 + type: date + description: chargebacks attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for int layer + risk.mart_chargebacks_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: date + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for mart layer + risk.raw_inventory_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: inventory attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for raw layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for raw layer + risk.stg_inventory_00: + owner_team: risk + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for stg layer + - name: col_01 + type: int + description: inventory attribute 1 for stg layer + - name: col_02 + type: int + description: inventory attribute 2 for stg layer + risk.int_inventory_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for int layer + - name: col_01 + type: varchar + description: inventory attribute 1 for int layer + - name: col_02 + type: int + description: inventory attribute 2 for int layer + risk.mart_inventory_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for mart layer + - name: col_01 + type: varchar + description: inventory attribute 1 for mart layer + - name: col_02 + type: int + description: inventory attribute 2 for mart layer + risk.raw_shipments_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for raw layer + - name: col_01 + type: date + description: shipments attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for raw layer + risk.stg_shipments_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for stg layer + - name: col_01 + type: varchar + description: shipments attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for stg layer + risk.int_shipments_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for int layer + - name: col_01 + type: int + description: shipments attribute 1 for int layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for int layer + risk.mart_shipments_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for mart layer + - name: col_02 + type: varchar + description: shipments attribute 2 for mart layer + risk.raw_carriers_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: carriers attribute 0 for raw layer + - name: col_01 + type: varchar + description: carriers attribute 1 for raw layer + - name: col_02 + type: date + description: carriers attribute 2 for raw layer + risk.stg_carriers_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for stg layer + - name: col_01 + type: date + description: carriers attribute 1 for stg layer + - name: col_02 + type: date + description: carriers attribute 2 for stg layer + risk.int_carriers_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for int layer + - name: col_01 + type: date + description: carriers attribute 1 for int layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for int layer + risk.mart_carriers_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for mart layer + - name: col_01 + type: int + description: carriers attribute 1 for mart layer + - name: col_02 + type: varchar + description: carriers attribute 2 for mart layer + risk.raw_products_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: products attribute 2 for raw layer + risk.stg_products_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for stg layer + - name: col_01 + type: int + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + risk.int_products_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for int layer + - name: col_01 + type: timestamp + description: products attribute 1 for int layer + - name: col_02 + type: timestamp + description: products attribute 2 for int layer + risk.mart_products_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for mart layer + - name: col_01 + type: timestamp + description: products attribute 1 for mart layer + - name: col_02 + type: int + description: products attribute 2 for mart layer + risk.raw_prices_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: varchar + description: prices attribute 2 for raw layer + risk.stg_prices_00: + owner_team: risk + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: prices attribute 0 for stg layer + - name: col_01 + type: varchar + description: prices attribute 1 for stg layer + - name: col_02 + type: varchar + description: prices attribute 2 for stg layer + risk.int_prices_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for int layer + - name: col_01 + type: date + description: prices attribute 1 for int layer + - name: col_02 + type: varchar + description: prices attribute 2 for int layer + risk.mart_prices_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for mart layer + - name: col_02 + type: date + description: prices attribute 2 for mart layer + risk.raw_queries_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for raw layer + - name: col_01 + type: timestamp + description: queries attribute 1 for raw layer + - name: col_02 + type: timestamp + description: queries attribute 2 for raw layer + risk.stg_queries_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: timestamp + description: queries attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for stg layer + risk.int_queries_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for int layer + - name: col_01 + type: date + description: queries attribute 1 for int layer + - name: col_02 + type: timestamp + description: queries attribute 2 for int layer + risk.mart_queries_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for mart layer + - name: col_01 + type: varchar + description: queries attribute 1 for mart layer + - name: col_02 + type: int + description: queries attribute 2 for mart layer + risk.raw_clicks_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for raw layer + risk.stg_clicks_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for stg layer + - name: col_01 + type: date + description: clicks attribute 1 for stg layer + - name: col_02 + type: varchar + description: clicks attribute 2 for stg layer + risk.int_clicks_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for int layer + - name: col_01 + type: date + description: clicks attribute 1 for int layer + - name: col_02 + type: int + description: clicks attribute 2 for int layer + risk.mart_clicks_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for mart layer + - name: col_01 + type: date + description: clicks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for mart layer + risk.raw_payouts_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for raw layer + - name: col_01 + type: varchar + description: payouts attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for raw layer + risk.stg_payouts_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: date + description: payouts attribute 1 for stg layer + - name: col_02 + type: int + description: payouts attribute 2 for stg layer + risk.int_payouts_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for int layer + - name: col_01 + type: date + description: payouts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for int layer + risk.mart_payouts_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for mart layer + - name: col_01 + type: varchar + description: payouts attribute 1 for mart layer + - name: col_02 + type: int + description: payouts attribute 2 for mart layer + risk.raw_settlements_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for raw layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for raw layer + risk.stg_settlements_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for stg layer + - name: col_01 + type: date + description: settlements attribute 1 for stg layer + - name: col_02 + type: varchar + description: settlements attribute 2 for stg layer + risk.int_settlements_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for int layer + risk.mart_settlements_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for mart layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for mart layer + - name: col_02 + type: date + description: settlements attribute 2 for mart layer + risk.raw_tickets_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: tickets attribute 0 for raw layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for raw layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for raw layer + risk.stg_tickets_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for stg layer + - name: col_02 + type: date + description: tickets attribute 2 for stg layer + risk.int_tickets_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: tickets attribute 0 for int layer + - name: col_01 + type: int + description: tickets attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for int layer + risk.mart_tickets_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for mart layer + - name: col_01 + type: varchar + description: tickets attribute 1 for mart layer + - name: col_02 + type: int + description: tickets attribute 2 for mart layer + risk.raw_csat_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for raw layer + - name: col_01 + type: int + description: csat attribute 1 for raw layer + - name: col_02 + type: date + description: csat attribute 2 for raw layer + risk.stg_csat_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for stg layer + - name: col_01 + type: date + description: csat attribute 1 for stg layer + - name: col_02 + type: timestamp + description: csat attribute 2 for stg layer + risk.int_csat_00: + owner_team: risk + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for int layer + - name: col_01 + type: date + description: csat attribute 1 for int layer + - name: col_02 + type: int + description: csat attribute 2 for int layer + risk.mart_csat_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: timestamp + description: csat attribute 2 for mart layer + risk.raw_headcount_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for raw layer + - name: col_01 + type: varchar + description: headcount attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for raw layer + risk.stg_headcount_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for stg layer + - name: col_01 + type: varchar + description: headcount attribute 1 for stg layer + - name: col_02 + type: varchar + description: headcount attribute 2 for stg layer + risk.int_headcount_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: int + description: headcount attribute 2 for int layer + risk.mart_headcount_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for mart layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for mart layer + - name: col_02 + type: varchar + description: headcount attribute 2 for mart layer + risk.raw_payroll_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for raw layer + - name: col_01 + type: varchar + description: payroll attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for raw layer + risk.stg_payroll_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: payroll attribute 0 for stg layer + - name: col_01 + type: date + description: payroll attribute 1 for stg layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for stg layer + risk.int_payroll_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for int layer + - name: col_01 + type: varchar + description: payroll attribute 1 for int layer + - name: col_02 + type: varchar + description: payroll attribute 2 for int layer + risk.mart_payroll_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for mart layer + - name: col_01 + type: varchar + description: payroll attribute 1 for mart layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for mart layer + risk.raw_forecast_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for raw layer + - name: col_01 + type: date + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + risk.stg_forecast_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for stg layer + risk.int_forecast_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for int layer + - name: col_01 + type: int + description: forecast attribute 1 for int layer + - name: col_02 + type: date + description: forecast attribute 2 for int layer + risk.mart_forecast_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for mart layer + - name: col_01 + type: date + description: forecast attribute 1 for mart layer + - name: col_02 + type: int + description: forecast attribute 2 for mart layer + risk.raw_returns_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for raw layer + - name: col_01 + type: date + description: returns attribute 1 for raw layer + - name: col_02 + type: timestamp + description: returns attribute 2 for raw layer + risk.stg_returns_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for stg layer + risk.int_returns_00: + owner_team: risk + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for int layer + - name: col_01 + type: timestamp + description: returns attribute 1 for int layer + - name: col_02 + type: date + description: returns attribute 2 for int layer + risk.mart_returns_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + risk.raw_suppliers_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for raw layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for raw layer + - name: col_02 + type: date + description: suppliers attribute 2 for raw layer + risk.stg_suppliers_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for stg layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for stg layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for stg layer + risk.int_suppliers_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for int layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for int layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for int layer + risk.mart_suppliers_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: int + description: suppliers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for mart layer + risk.raw_warehouses_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for raw layer + - name: col_01 + type: date + description: warehouses attribute 1 for raw layer + - name: col_02 + type: int + description: warehouses attribute 2 for raw layer + risk.stg_warehouses_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for stg layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for stg layer + risk.int_warehouses_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for int layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for int layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for int layer + risk.mart_warehouses_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for mart layer + - name: col_01 + type: int + description: warehouses attribute 1 for mart layer + - name: col_02 + type: date + description: warehouses attribute 2 for mart layer + risk.raw_promotions_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for raw layer + risk.stg_promotions_00: + owner_team: risk + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: promotions attribute 0 for stg layer + - name: col_01 + type: int + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + risk.int_promotions_00: + owner_team: risk + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for int layer + - name: col_02 + type: date + description: promotions attribute 2 for int layer + risk.mart_promotions_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for mart layer + - name: col_02 + type: varchar + description: promotions attribute 2 for mart layer + risk.raw_attribution_00: + owner_team: risk + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for raw layer + - name: col_01 + type: int + description: attribution attribute 1 for raw layer + - name: col_02 + type: varchar + description: attribution attribute 2 for raw layer + risk.stg_attribution_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for stg layer + - name: col_01 + type: varchar + description: attribution attribute 1 for stg layer + - name: col_02 + type: int + description: attribution attribute 2 for stg layer + risk.int_attribution_00: + owner_team: risk + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: attribution attribute 0 for int layer + - name: col_01 + type: date + description: attribution attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for int layer + risk.mart_attribution_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: varchar + description: attribution attribute 2 for mart layer + risk.raw_cohorts_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for raw layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for raw layer + - name: col_02 + type: int + description: cohorts attribute 2 for raw layer + risk.stg_cohorts_00: + owner_team: risk + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for stg layer + - name: col_01 + type: int + description: cohorts attribute 1 for stg layer + - name: col_02 + type: int + description: cohorts attribute 2 for stg layer + risk.int_cohorts_00: + owner_team: risk + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for int layer + - name: col_01 + type: int + description: cohorts attribute 1 for int layer + - name: col_02 + type: date + description: cohorts attribute 2 for int layer + risk.mart_cohorts_00: + owner_team: risk + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for mart layer + - name: col_02 + type: date + description: cohorts attribute 2 for mart layer + supply.raw_orders_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for raw layer + - name: col_01 + type: varchar + description: orders attribute 1 for raw layer + - name: col_02 + type: varchar + description: orders attribute 2 for raw layer + supply.stg_orders_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: date + description: orders attribute 1 for stg layer + - name: col_02 + type: date + description: orders attribute 2 for stg layer + supply.int_orders_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: orders attribute 0 for int layer + - name: col_01 + type: timestamp + description: orders attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for int layer + supply.mart_orders_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for mart layer + - name: col_01 + type: int + description: orders attribute 1 for mart layer + - name: col_02 + type: int + description: orders attribute 2 for mart layer + supply.raw_invoices_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for raw layer + - name: col_02 + type: varchar + description: invoices attribute 2 for raw layer + supply.stg_invoices_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for stg layer + - name: col_01 + type: varchar + description: invoices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for stg layer + supply.int_invoices_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for int layer + - name: col_01 + type: date + description: invoices attribute 1 for int layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for int layer + supply.mart_invoices_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for mart layer + - name: col_01 + type: int + description: invoices attribute 1 for mart layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for mart layer + supply.raw_ledger_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for raw layer + - name: col_01 + type: date + description: ledger attribute 1 for raw layer + - name: col_02 + type: varchar + description: ledger attribute 2 for raw layer + supply.stg_ledger_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for stg layer + - name: col_02 + type: varchar + description: ledger attribute 2 for stg layer + supply.int_ledger_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for int layer + - name: col_01 + type: varchar + description: ledger attribute 1 for int layer + - name: col_02 + type: varchar + description: ledger attribute 2 for int layer + supply.mart_ledger_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for mart layer + - name: col_01 + type: varchar + description: ledger attribute 1 for mart layer + - name: col_02 + type: int + description: ledger attribute 2 for mart layer + supply.raw_refunds_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for raw layer + - name: col_01 + type: int + description: refunds attribute 1 for raw layer + - name: col_02 + type: date + description: refunds attribute 2 for raw layer + supply.stg_refunds_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for stg layer + - name: col_01 + type: varchar + description: refunds attribute 1 for stg layer + - name: col_02 + type: int + description: refunds attribute 2 for stg layer + supply.int_refunds_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for int layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for int layer + - name: col_02 + type: int + description: refunds attribute 2 for int layer + supply.mart_refunds_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for mart layer + - name: col_01 + type: date + description: refunds attribute 1 for mart layer + - name: col_02 + type: date + description: refunds attribute 2 for mart layer + supply.raw_sessions_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: int + description: sessions attribute 2 for raw layer + supply.stg_sessions_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for stg layer + - name: col_01 + type: varchar + description: sessions attribute 1 for stg layer + - name: col_02 + type: date + description: sessions attribute 2 for stg layer + supply.int_sessions_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for int layer + - name: col_01 + type: varchar + description: sessions attribute 1 for int layer + - name: col_02 + type: date + description: sessions attribute 2 for int layer + supply.mart_sessions_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: date + description: sessions attribute 1 for mart layer + - name: col_02 + type: varchar + description: sessions attribute 2 for mart layer + supply.raw_signups_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for raw layer + - name: col_01 + type: timestamp + description: signups attribute 1 for raw layer + - name: col_02 + type: varchar + description: signups attribute 2 for raw layer + supply.stg_signups_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for stg layer + - name: col_01 + type: date + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + supply.int_signups_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for int layer + - name: col_01 + type: date + description: signups attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for int layer + supply.mart_signups_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for mart layer + - name: col_01 + type: date + description: signups attribute 1 for mart layer + - name: col_02 + type: timestamp + description: signups attribute 2 for mart layer + supply.raw_campaigns_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for raw layer + - name: col_02 + type: int + description: campaigns attribute 2 for raw layer + supply.stg_campaigns_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for stg layer + - name: col_02 + type: int + description: campaigns attribute 2 for stg layer + supply.int_campaigns_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for int layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for int layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for int layer + supply.mart_campaigns_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for mart layer + - name: col_02 + type: int + description: campaigns attribute 2 for mart layer + supply.raw_spend_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: spend attribute 0 for raw layer + - name: col_01 + type: varchar + description: spend attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for raw layer + supply.stg_spend_00: + owner_team: supply + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for stg layer + - name: col_01 + type: varchar + description: spend attribute 1 for stg layer + - name: col_02 + type: date + description: spend attribute 2 for stg layer + supply.int_spend_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: spend attribute 0 for int layer + - name: col_01 + type: date + description: spend attribute 1 for int layer + - name: col_02 + type: int + description: spend attribute 2 for int layer + supply.mart_spend_00: + owner_team: supply + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for mart layer + - name: col_01 + type: varchar + description: spend attribute 1 for mart layer + - name: col_02 + type: timestamp + description: spend attribute 2 for mart layer + supply.raw_fraud_scores_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for raw layer + supply.stg_fraud_scores_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for stg layer + supply.int_fraud_scores_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for int layer + supply.mart_fraud_scores_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for mart layer + supply.raw_chargebacks_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for raw layer + supply.stg_chargebacks_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: date + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for stg layer + supply.int_chargebacks_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for int layer + - name: col_02 + type: date + description: chargebacks attribute 2 for int layer + supply.mart_chargebacks_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for mart layer + supply.raw_inventory_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for raw layer + - name: col_02 + type: varchar + description: inventory attribute 2 for raw layer + supply.stg_inventory_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for stg layer + - name: col_01 + type: date + description: inventory attribute 1 for stg layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for stg layer + supply.int_inventory_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for int layer + - name: col_01 + type: int + description: inventory attribute 1 for int layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for int layer + supply.mart_inventory_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for mart layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for mart layer + - name: col_02 + type: varchar + description: inventory attribute 2 for mart layer + supply.raw_shipments_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for raw layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for raw layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for raw layer + supply.stg_shipments_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + supply.int_shipments_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for int layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for int layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for int layer + supply.mart_shipments_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for mart layer + - name: col_01 + type: varchar + description: shipments attribute 1 for mart layer + - name: col_02 + type: int + description: shipments attribute 2 for mart layer + supply.raw_carriers_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for raw layer + - name: col_01 + type: date + description: carriers attribute 1 for raw layer + - name: col_02 + type: varchar + description: carriers attribute 2 for raw layer + supply.stg_carriers_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: carriers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for stg layer + - name: col_02 + type: varchar + description: carriers attribute 2 for stg layer + supply.int_carriers_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: carriers attribute 0 for int layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for int layer + - name: col_02 + type: varchar + description: carriers attribute 2 for int layer + supply.mart_carriers_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for mart layer + supply.raw_products_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for raw layer + - name: col_01 + type: timestamp + description: products attribute 1 for raw layer + - name: col_02 + type: timestamp + description: products attribute 2 for raw layer + supply.stg_products_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: products attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for stg layer + - name: col_02 + type: timestamp + description: products attribute 2 for stg layer + supply.int_products_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for int layer + - name: col_02 + type: int + description: products attribute 2 for int layer + supply.mart_products_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for mart layer + - name: col_01 + type: date + description: products attribute 1 for mart layer + - name: col_02 + type: date + description: products attribute 2 for mart layer + supply.raw_prices_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: date + description: prices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: prices attribute 2 for raw layer + supply.stg_prices_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for stg layer + - name: col_01 + type: varchar + description: prices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: prices attribute 2 for stg layer + supply.int_prices_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for int layer + - name: col_01 + type: int + description: prices attribute 1 for int layer + - name: col_02 + type: varchar + description: prices attribute 2 for int layer + supply.mart_prices_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: prices attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for mart layer + supply.raw_queries_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for raw layer + - name: col_01 + type: date + description: queries attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for raw layer + supply.stg_queries_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for stg layer + - name: col_02 + type: int + description: queries attribute 2 for stg layer + supply.int_queries_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for int layer + - name: col_01 + type: int + description: queries attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for int layer + supply.mart_queries_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for mart layer + - name: col_01 + type: timestamp + description: queries attribute 1 for mart layer + - name: col_02 + type: varchar + description: queries attribute 2 for mart layer + supply.raw_clicks_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: date + description: clicks attribute 2 for raw layer + supply.stg_clicks_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for stg layer + - name: col_01 + type: timestamp + description: clicks attribute 1 for stg layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for stg layer + supply.int_clicks_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: clicks attribute 0 for int layer + - name: col_01 + type: timestamp + description: clicks attribute 1 for int layer + - name: col_02 + type: int + description: clicks attribute 2 for int layer + supply.mart_clicks_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for mart layer + - name: col_02 + type: varchar + description: clicks attribute 2 for mart layer + supply.raw_payouts_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for raw layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for raw layer + supply.stg_payouts_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for stg layer + - name: col_01 + type: date + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + supply.int_payouts_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for int layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for int layer + - name: col_02 + type: date + description: payouts attribute 2 for int layer + supply.mart_payouts_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for mart layer + - name: col_01 + type: varchar + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + supply.raw_settlements_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for raw layer + - name: col_01 + type: varchar + description: settlements attribute 1 for raw layer + - name: col_02 + type: varchar + description: settlements attribute 2 for raw layer + supply.stg_settlements_00: + owner_team: supply + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: settlements attribute 0 for stg layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for stg layer + - name: col_02 + type: int + description: settlements attribute 2 for stg layer + supply.int_settlements_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for int layer + - name: col_01 + type: varchar + description: settlements attribute 1 for int layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for int layer + supply.mart_settlements_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: settlements attribute 0 for mart layer + - name: col_01 + type: date + description: settlements attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for mart layer + supply.raw_tickets_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for raw layer + - name: col_01 + type: int + description: tickets attribute 1 for raw layer + - name: col_02 + type: varchar + description: tickets attribute 2 for raw layer + supply.stg_tickets_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for stg layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for stg layer + - name: col_02 + type: varchar + description: tickets attribute 2 for stg layer + supply.int_tickets_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for int layer + - name: col_01 + type: date + description: tickets attribute 1 for int layer + - name: col_02 + type: varchar + description: tickets attribute 2 for int layer + supply.mart_tickets_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: tickets attribute 0 for mart layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for mart layer + - name: col_02 + type: varchar + description: tickets attribute 2 for mart layer + supply.raw_csat_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for raw layer + - name: col_01 + type: timestamp + description: csat attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for raw layer + supply.stg_csat_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for stg layer + - name: col_01 + type: date + description: csat attribute 1 for stg layer + - name: col_02 + type: varchar + description: csat attribute 2 for stg layer + supply.int_csat_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for int layer + supply.mart_csat_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: int + description: csat attribute 2 for mart layer + supply.raw_headcount_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for raw layer + - name: col_01 + type: int + description: headcount attribute 1 for raw layer + - name: col_02 + type: date + description: headcount attribute 2 for raw layer + supply.stg_headcount_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for stg layer + - name: col_01 + type: int + description: headcount attribute 1 for stg layer + - name: col_02 + type: date + description: headcount attribute 2 for stg layer + supply.int_headcount_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for int layer + supply.mart_headcount_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for mart layer + - name: col_01 + type: int + description: headcount attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for mart layer + supply.raw_payroll_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for raw layer + - name: col_02 + type: varchar + description: payroll attribute 2 for raw layer + supply.stg_payroll_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for stg layer + - name: col_02 + type: date + description: payroll attribute 2 for stg layer + supply.int_payroll_00: + owner_team: supply + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: date + description: payroll attribute 1 for int layer + - name: col_02 + type: date + description: payroll attribute 2 for int layer + supply.mart_payroll_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: payroll attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for mart layer + - name: col_02 + type: int + description: payroll attribute 2 for mart layer + supply.raw_forecast_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for raw layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for raw layer + - name: col_02 + type: int + description: forecast attribute 2 for raw layer + supply.stg_forecast_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for stg layer + - name: col_01 + type: varchar + description: forecast attribute 1 for stg layer + - name: col_02 + type: varchar + description: forecast attribute 2 for stg layer + supply.int_forecast_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for int layer + - name: col_01 + type: timestamp + description: forecast attribute 1 for int layer + - name: col_02 + type: timestamp + description: forecast attribute 2 for int layer + supply.mart_forecast_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for mart layer + - name: col_02 + type: timestamp + description: forecast attribute 2 for mart layer + supply.raw_returns_00: + owner_team: supply + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for raw layer + - name: col_01 + type: int + description: returns attribute 1 for raw layer + - name: col_02 + type: int + description: returns attribute 2 for raw layer + supply.stg_returns_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: returns attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for stg layer + supply.int_returns_00: + owner_team: supply + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: returns attribute 0 for int layer + - name: col_01 + type: date + description: returns attribute 1 for int layer + - name: col_02 + type: timestamp + description: returns attribute 2 for int layer + supply.mart_returns_00: + owner_team: supply + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for mart layer + - name: col_01 + type: varchar + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + supply.raw_suppliers_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for raw layer + - name: col_02 + type: int + description: suppliers attribute 2 for raw layer + supply.stg_suppliers_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for stg layer + - name: col_02 + type: date + description: suppliers attribute 2 for stg layer + supply.int_suppliers_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: suppliers attribute 0 for int layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for int layer + - name: col_02 + type: int + description: suppliers attribute 2 for int layer + supply.mart_suppliers_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: suppliers attribute 0 for mart layer + - name: col_01 + type: date + description: suppliers attribute 1 for mart layer + - name: col_02 + type: int + description: suppliers attribute 2 for mart layer + supply.raw_warehouses_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for raw layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for raw layer + - name: col_02 + type: date + description: warehouses attribute 2 for raw layer + supply.stg_warehouses_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for stg layer + - name: col_01 + type: int + description: warehouses attribute 1 for stg layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for stg layer + supply.int_warehouses_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for int layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for int layer + supply.mart_warehouses_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for mart layer + - name: col_01 + type: date + description: warehouses attribute 1 for mart layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for mart layer + supply.raw_promotions_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for raw layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for raw layer + supply.stg_promotions_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for stg layer + - name: col_02 + type: varchar + description: promotions attribute 2 for stg layer + supply.int_promotions_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for int layer + - name: col_01 + type: int + description: promotions attribute 1 for int layer + - name: col_02 + type: date + description: promotions attribute 2 for int layer + supply.mart_promotions_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for mart layer + - name: col_01 + type: varchar + description: promotions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for mart layer + supply.raw_attribution_00: + owner_team: supply + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for raw layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for raw layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for raw layer + supply.stg_attribution_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for stg layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for stg layer + - name: col_02 + type: date + description: attribution attribute 2 for stg layer + supply.int_attribution_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: attribution attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for int layer + supply.mart_attribution_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for mart layer + - name: col_02 + type: int + description: attribution attribute 2 for mart layer + supply.raw_cohorts_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for raw layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for raw layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for raw layer + supply.stg_cohorts_00: + owner_team: supply + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for stg layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for stg layer + - name: col_02 + type: date + description: cohorts attribute 2 for stg layer + supply.int_cohorts_00: + owner_team: supply + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for int layer + - name: col_01 + type: date + description: cohorts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for int layer + supply.mart_cohorts_00: + owner_team: supply + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for mart layer + - name: col_01 + type: date + description: cohorts attribute 1 for mart layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for mart layer + logistics.raw_orders_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for raw layer + - name: col_02 + type: int + description: orders attribute 2 for raw layer + logistics.stg_orders_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for stg layer + - name: col_02 + type: timestamp + description: orders attribute 2 for stg layer + logistics.int_orders_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for int layer + - name: col_01 + type: date + description: orders attribute 1 for int layer + - name: col_02 + type: int + description: orders attribute 2 for int layer + logistics.mart_orders_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for mart layer + - name: col_01 + type: date + description: orders attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for mart layer + logistics.raw_invoices_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for raw layer + - name: col_01 + type: date + description: invoices attribute 1 for raw layer + - name: col_02 + type: varchar + description: invoices attribute 2 for raw layer + logistics.stg_invoices_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for stg layer + - name: col_02 + type: varchar + description: invoices attribute 2 for stg layer + logistics.int_invoices_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for int layer + - name: col_01 + type: int + description: invoices attribute 1 for int layer + - name: col_02 + type: date + description: invoices attribute 2 for int layer + logistics.mart_invoices_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for mart layer + - name: col_01 + type: varchar + description: invoices attribute 1 for mart layer + - name: col_02 + type: int + description: invoices attribute 2 for mart layer + logistics.raw_ledger_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for raw layer + - name: col_01 + type: varchar + description: ledger attribute 1 for raw layer + - name: col_02 + type: varchar + description: ledger attribute 2 for raw layer + logistics.stg_ledger_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for stg layer + - name: col_01 + type: date + description: ledger attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for stg layer + logistics.int_ledger_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for int layer + - name: col_01 + type: date + description: ledger attribute 1 for int layer + - name: col_02 + type: varchar + description: ledger attribute 2 for int layer + logistics.mart_ledger_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for mart layer + - name: col_01 + type: int + description: ledger attribute 1 for mart layer + - name: col_02 + type: date + description: ledger attribute 2 for mart layer + logistics.raw_refunds_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for raw layer + - name: col_02 + type: date + description: refunds attribute 2 for raw layer + logistics.stg_refunds_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for stg layer + - name: col_01 + type: date + description: refunds attribute 1 for stg layer + - name: col_02 + type: date + description: refunds attribute 2 for stg layer + logistics.int_refunds_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: refunds attribute 0 for int layer + - name: col_01 + type: int + description: refunds attribute 1 for int layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for int layer + logistics.mart_refunds_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for mart layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for mart layer + - name: col_02 + type: date + description: refunds attribute 2 for mart layer + logistics.raw_sessions_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for raw layer + - name: col_01 + type: date + description: sessions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for raw layer + logistics.stg_sessions_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for stg layer + - name: col_02 + type: int + description: sessions attribute 2 for stg layer + logistics.int_sessions_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for int layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for int layer + - name: col_02 + type: date + description: sessions attribute 2 for int layer + logistics.mart_sessions_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for mart layer + - name: col_02 + type: int + description: sessions attribute 2 for mart layer + logistics.raw_signups_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for raw layer + - name: col_01 + type: date + description: signups attribute 1 for raw layer + - name: col_02 + type: int + description: signups attribute 2 for raw layer + logistics.stg_signups_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for stg layer + - name: col_01 + type: varchar + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + logistics.int_signups_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for int layer + logistics.mart_signups_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for mart layer + - name: col_01 + type: varchar + description: signups attribute 1 for mart layer + - name: col_02 + type: varchar + description: signups attribute 2 for mart layer + logistics.raw_campaigns_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for raw layer + - name: col_01 + type: date + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + logistics.stg_campaigns_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for stg layer + - name: col_02 + type: date + description: campaigns attribute 2 for stg layer + logistics.int_campaigns_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for int layer + - name: col_01 + type: int + description: campaigns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for int layer + logistics.mart_campaigns_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for mart layer + - name: col_02 + type: int + description: campaigns attribute 2 for mart layer + logistics.raw_spend_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for raw layer + - name: col_01 + type: timestamp + description: spend attribute 1 for raw layer + - name: col_02 + type: int + description: spend attribute 2 for raw layer + logistics.stg_spend_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: spend attribute 0 for stg layer + - name: col_01 + type: varchar + description: spend attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for stg layer + logistics.int_spend_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for int layer + - name: col_01 + type: int + description: spend attribute 1 for int layer + - name: col_02 + type: varchar + description: spend attribute 2 for int layer + logistics.mart_spend_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for mart layer + - name: col_02 + type: timestamp + description: spend attribute 2 for mart layer + logistics.raw_fraud_scores_00: + owner_team: logistics + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for raw layer + logistics.stg_fraud_scores_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for stg layer + logistics.int_fraud_scores_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for int layer + logistics.mart_fraud_scores_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for mart layer + logistics.raw_chargebacks_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: date + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: date + description: chargebacks attribute 2 for raw layer + logistics.stg_chargebacks_00: + owner_team: logistics + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: int + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for stg layer + logistics.int_chargebacks_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for int layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for int layer + logistics.mart_chargebacks_00: + owner_team: logistics + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: int + description: chargebacks attribute 2 for mart layer + logistics.raw_inventory_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for raw layer + - name: col_01 + type: date + description: inventory attribute 1 for raw layer + - name: col_02 + type: varchar + description: inventory attribute 2 for raw layer + logistics.stg_inventory_00: + owner_team: logistics + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for stg layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for stg layer + logistics.int_inventory_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for int layer + - name: col_02 + type: int + description: inventory attribute 2 for int layer + logistics.mart_inventory_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for mart layer + - name: col_02 + type: int + description: inventory attribute 2 for mart layer + logistics.raw_shipments_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for raw layer + - name: col_01 + type: varchar + description: shipments attribute 1 for raw layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for raw layer + logistics.stg_shipments_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for stg layer + - name: col_01 + type: varchar + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + logistics.int_shipments_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for int layer + - name: col_01 + type: date + description: shipments attribute 1 for int layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for int layer + logistics.mart_shipments_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for mart layer + - name: col_01 + type: varchar + description: shipments attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for mart layer + logistics.raw_carriers_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for raw layer + - name: col_01 + type: int + description: carriers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for raw layer + logistics.stg_carriers_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for stg layer + - name: col_01 + type: date + description: carriers attribute 1 for stg layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for stg layer + logistics.int_carriers_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: carriers attribute 0 for int layer + - name: col_01 + type: varchar + description: carriers attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for int layer + logistics.mart_carriers_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: date + description: carriers attribute 2 for mart layer + logistics.raw_products_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for raw layer + - name: col_01 + type: date + description: products attribute 1 for raw layer + - name: col_02 + type: timestamp + description: products attribute 2 for raw layer + logistics.stg_products_00: + owner_team: logistics + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + logistics.int_products_00: + owner_team: logistics + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: products attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for int layer + - name: col_02 + type: date + description: products attribute 2 for int layer + logistics.mart_products_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for mart layer + - name: col_01 + type: int + description: products attribute 1 for mart layer + - name: col_02 + type: int + description: products attribute 2 for mart layer + logistics.raw_prices_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: int + description: prices attribute 2 for raw layer + logistics.stg_prices_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for stg layer + - name: col_02 + type: int + description: prices attribute 2 for stg layer + logistics.int_prices_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: prices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for int layer + - name: col_02 + type: timestamp + description: prices attribute 2 for int layer + logistics.mart_prices_00: + owner_team: logistics + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: prices attribute 0 for mart layer + - name: col_01 + type: int + description: prices attribute 1 for mart layer + - name: col_02 + type: varchar + description: prices attribute 2 for mart layer + logistics.raw_queries_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for raw layer + - name: col_01 + type: timestamp + description: queries attribute 1 for raw layer + - name: col_02 + type: timestamp + description: queries attribute 2 for raw layer + logistics.stg_queries_00: + owner_team: logistics + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for stg layer + - name: col_02 + type: timestamp + description: queries attribute 2 for stg layer + logistics.int_queries_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: queries attribute 0 for int layer + - name: col_01 + type: timestamp + description: queries attribute 1 for int layer + - name: col_02 + type: varchar + description: queries attribute 2 for int layer + logistics.mart_queries_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for mart layer + - name: col_01 + type: timestamp + description: queries attribute 1 for mart layer + - name: col_02 + type: date + description: queries attribute 2 for mart layer + logistics.raw_clicks_00: + owner_team: logistics + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for raw layer + - name: col_01 + type: date + description: clicks attribute 1 for raw layer + - name: col_02 + type: date + description: clicks attribute 2 for raw layer + logistics.stg_clicks_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: int + description: clicks attribute 2 for stg layer + logistics.int_clicks_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for int layer + - name: col_01 + type: date + description: clicks attribute 1 for int layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for int layer + logistics.mart_clicks_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for mart layer + - name: col_01 + type: date + description: clicks attribute 1 for mart layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for mart layer + logistics.raw_payouts_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for raw layer + - name: col_01 + type: date + description: payouts attribute 1 for raw layer + - name: col_02 + type: int + description: payouts attribute 2 for raw layer + logistics.stg_payouts_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for stg layer + - name: col_02 + type: int + description: payouts attribute 2 for stg layer + logistics.int_payouts_00: + owner_team: logistics + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for int layer + - name: col_01 + type: varchar + description: payouts attribute 1 for int layer + - name: col_02 + type: date + description: payouts attribute 2 for int layer + logistics.mart_payouts_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for mart layer + - name: col_02 + type: varchar + description: payouts attribute 2 for mart layer + logistics.raw_settlements_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for raw layer + - name: col_01 + type: date + description: settlements attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for raw layer + logistics.stg_settlements_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for stg layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for stg layer + - name: col_02 + type: date + description: settlements attribute 2 for stg layer + logistics.int_settlements_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for int layer + logistics.mart_settlements_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for mart layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for mart layer + - name: col_02 + type: varchar + description: settlements attribute 2 for mart layer + logistics.raw_tickets_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for raw layer + - name: col_01 + type: int + description: tickets attribute 1 for raw layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for raw layer + logistics.stg_tickets_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for stg layer + logistics.int_tickets_00: + owner_team: logistics + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for int layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for int layer + logistics.mart_tickets_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for mart layer + - name: col_02 + type: int + description: tickets attribute 2 for mart layer + logistics.raw_csat_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for raw layer + - name: col_01 + type: int + description: csat attribute 1 for raw layer + - name: col_02 + type: timestamp + description: csat attribute 2 for raw layer + logistics.stg_csat_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for stg layer + - name: col_01 + type: int + description: csat attribute 1 for stg layer + - name: col_02 + type: varchar + description: csat attribute 2 for stg layer + logistics.int_csat_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for int layer + - name: col_01 + type: int + description: csat attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for int layer + logistics.mart_csat_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: date + description: csat attribute 2 for mart layer + logistics.raw_headcount_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for raw layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for raw layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for raw layer + logistics.stg_headcount_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: headcount attribute 0 for stg layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for stg layer + logistics.int_headcount_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for int layer + - name: col_01 + type: date + description: headcount attribute 1 for int layer + - name: col_02 + type: date + description: headcount attribute 2 for int layer + logistics.mart_headcount_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for mart layer + - name: col_01 + type: date + description: headcount attribute 1 for mart layer + - name: col_02 + type: int + description: headcount attribute 2 for mart layer + logistics.raw_payroll_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for raw layer + - name: col_01 + type: varchar + description: payroll attribute 1 for raw layer + - name: col_02 + type: int + description: payroll attribute 2 for raw layer + logistics.stg_payroll_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for stg layer + - name: col_01 + type: varchar + description: payroll attribute 1 for stg layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for stg layer + logistics.int_payroll_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for int layer + - name: col_01 + type: int + description: payroll attribute 1 for int layer + - name: col_02 + type: varchar + description: payroll attribute 2 for int layer + logistics.mart_payroll_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for mart layer + - name: col_01 + type: int + description: payroll attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for mart layer + logistics.raw_forecast_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for raw layer + - name: col_01 + type: varchar + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + logistics.stg_forecast_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for stg layer + - name: col_01 + type: date + description: forecast attribute 1 for stg layer + - name: col_02 + type: varchar + description: forecast attribute 2 for stg layer + logistics.int_forecast_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for int layer + logistics.mart_forecast_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for mart layer + - name: col_01 + type: date + description: forecast attribute 1 for mart layer + - name: col_02 + type: int + description: forecast attribute 2 for mart layer + logistics.raw_returns_00: + owner_team: logistics + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: timestamp + description: returns attribute 2 for raw layer + logistics.stg_returns_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: returns attribute 1 for stg layer + - name: col_02 + type: int + description: returns attribute 2 for stg layer + logistics.int_returns_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for int layer + - name: col_01 + type: timestamp + description: returns attribute 1 for int layer + - name: col_02 + type: int + description: returns attribute 2 for int layer + logistics.mart_returns_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for mart layer + - name: col_01 + type: int + description: returns attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for mart layer + logistics.raw_suppliers_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for raw layer + - name: col_01 + type: int + description: suppliers attribute 1 for raw layer + - name: col_02 + type: date + description: suppliers attribute 2 for raw layer + logistics.stg_suppliers_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for stg layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for stg layer + logistics.int_suppliers_00: + owner_team: logistics + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for int layer + - name: col_01 + type: int + description: suppliers attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for int layer + logistics.mart_suppliers_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for mart layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for mart layer + logistics.raw_warehouses_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for raw layer + logistics.stg_warehouses_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for stg layer + - name: col_01 + type: date + description: warehouses attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for stg layer + logistics.int_warehouses_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for int layer + - name: col_01 + type: date + description: warehouses attribute 1 for int layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for int layer + logistics.mart_warehouses_00: + owner_team: logistics + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for mart layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for mart layer + logistics.raw_promotions_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for raw layer + - name: col_02 + type: date + description: promotions attribute 2 for raw layer + logistics.stg_promotions_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for stg layer + - name: col_02 + type: varchar + description: promotions attribute 2 for stg layer + logistics.int_promotions_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for int layer + - name: col_02 + type: int + description: promotions attribute 2 for int layer + logistics.mart_promotions_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for mart layer + - name: col_01 + type: varchar + description: promotions attribute 1 for mart layer + - name: col_02 + type: date + description: promotions attribute 2 for mart layer + logistics.raw_attribution_00: + owner_team: logistics + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for raw layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for raw layer + - name: col_02 + type: varchar + description: attribution attribute 2 for raw layer + logistics.stg_attribution_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for stg layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for stg layer + logistics.int_attribution_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for int layer + - name: col_01 + type: timestamp + description: attribution attribute 1 for int layer + - name: col_02 + type: varchar + description: attribution attribute 2 for int layer + logistics.mart_attribution_00: + owner_team: logistics + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: attribution attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for mart layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for mart layer + logistics.raw_cohorts_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for raw layer + - name: col_01 + type: int + description: cohorts attribute 1 for raw layer + - name: col_02 + type: int + description: cohorts attribute 2 for raw layer + logistics.stg_cohorts_00: + owner_team: logistics + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for stg layer + - name: col_02 + type: date + description: cohorts attribute 2 for stg layer + logistics.int_cohorts_00: + owner_team: logistics + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for int layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for int layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for int layer + logistics.mart_cohorts_00: + owner_team: logistics + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for mart layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for mart layer + - name: col_02 + type: date + description: cohorts attribute 2 for mart layer + catalog.raw_orders_00: + owner_team: catalog + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: orders attribute 0 for raw layer + - name: col_01 + type: varchar + description: orders attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for raw layer + catalog.stg_orders_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: timestamp + description: orders attribute 1 for stg layer + - name: col_02 + type: timestamp + description: orders attribute 2 for stg layer + catalog.int_orders_00: + owner_team: catalog + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: orders attribute 0 for int layer + - name: col_01 + type: date + description: orders attribute 1 for int layer + - name: col_02 + type: date + description: orders attribute 2 for int layer + catalog.mart_orders_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for mart layer + - name: col_01 + type: timestamp + description: orders attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for mart layer + catalog.raw_invoices_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for raw layer + - name: col_01 + type: int + description: invoices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for raw layer + catalog.stg_invoices_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for stg layer + - name: col_01 + type: int + description: invoices attribute 1 for stg layer + - name: col_02 + type: int + description: invoices attribute 2 for stg layer + catalog.int_invoices_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for int layer + - name: col_01 + type: varchar + description: invoices attribute 1 for int layer + - name: col_02 + type: int + description: invoices attribute 2 for int layer + catalog.mart_invoices_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for mart layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for mart layer + - name: col_02 + type: varchar + description: invoices attribute 2 for mart layer + catalog.raw_ledger_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for raw layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for raw layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for raw layer + catalog.stg_ledger_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for stg layer + - name: col_01 + type: int + description: ledger attribute 1 for stg layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for stg layer + catalog.int_ledger_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: ledger attribute 0 for int layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for int layer + - name: col_02 + type: int + description: ledger attribute 2 for int layer + catalog.mart_ledger_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for mart layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for mart layer + - name: col_02 + type: int + description: ledger attribute 2 for mart layer + catalog.raw_refunds_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for raw layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for raw layer + - name: col_02 + type: varchar + description: refunds attribute 2 for raw layer + catalog.stg_refunds_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for stg layer + - name: col_01 + type: int + description: refunds attribute 1 for stg layer + - name: col_02 + type: date + description: refunds attribute 2 for stg layer + catalog.int_refunds_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for int layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for int layer + - name: col_02 + type: varchar + description: refunds attribute 2 for int layer + catalog.mart_refunds_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for mart layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for mart layer + - name: col_02 + type: date + description: refunds attribute 2 for mart layer + catalog.raw_sessions_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for raw layer + - name: col_01 + type: int + description: sessions attribute 1 for raw layer + - name: col_02 + type: date + description: sessions attribute 2 for raw layer + catalog.stg_sessions_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for stg layer + - name: col_01 + type: varchar + description: sessions attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for stg layer + catalog.int_sessions_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for int layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for int layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for int layer + catalog.mart_sessions_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: varchar + description: sessions attribute 1 for mart layer + - name: col_02 + type: int + description: sessions attribute 2 for mart layer + catalog.raw_signups_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: signups attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for raw layer + - name: col_02 + type: timestamp + description: signups attribute 2 for raw layer + catalog.stg_signups_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + catalog.int_signups_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for int layer + - name: col_01 + type: date + description: signups attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for int layer + catalog.mart_signups_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for mart layer + - name: col_01 + type: timestamp + description: signups attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for mart layer + catalog.raw_campaigns_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for raw layer + - name: col_01 + type: int + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + catalog.stg_campaigns_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for stg layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for stg layer + - name: col_02 + type: int + description: campaigns attribute 2 for stg layer + catalog.int_campaigns_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for int layer + catalog.mart_campaigns_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for mart layer + - name: col_01 + type: date + description: campaigns attribute 1 for mart layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for mart layer + catalog.raw_spend_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: varchar + description: spend attribute 1 for raw layer + - name: col_02 + type: timestamp + description: spend attribute 2 for raw layer + catalog.stg_spend_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for stg layer + - name: col_01 + type: date + description: spend attribute 1 for stg layer + - name: col_02 + type: timestamp + description: spend attribute 2 for stg layer + catalog.int_spend_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for int layer + - name: col_01 + type: timestamp + description: spend attribute 1 for int layer + - name: col_02 + type: varchar + description: spend attribute 2 for int layer + catalog.mart_spend_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for mart layer + - name: col_01 + type: varchar + description: spend attribute 1 for mart layer + - name: col_02 + type: timestamp + description: spend attribute 2 for mart layer + catalog.raw_fraud_scores_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for raw layer + catalog.stg_fraud_scores_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for stg layer + catalog.int_fraud_scores_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for int layer + catalog.mart_fraud_scores_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for mart layer + catalog.raw_chargebacks_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for raw layer + catalog.stg_chargebacks_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: date + description: chargebacks attribute 2 for stg layer + catalog.int_chargebacks_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for int layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for int layer + catalog.mart_chargebacks_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for mart layer + catalog.raw_inventory_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for raw layer + - name: col_01 + type: date + description: inventory attribute 1 for raw layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for raw layer + catalog.stg_inventory_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for stg layer + - name: col_01 + type: varchar + description: inventory attribute 1 for stg layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for stg layer + catalog.int_inventory_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for int layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for int layer + catalog.mart_inventory_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for mart layer + - name: col_01 + type: varchar + description: inventory attribute 1 for mart layer + - name: col_02 + type: date + description: inventory attribute 2 for mart layer + catalog.raw_shipments_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: shipments attribute 0 for raw layer + - name: col_01 + type: varchar + description: shipments attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for raw layer + catalog.stg_shipments_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for stg layer + - name: col_01 + type: date + description: shipments attribute 1 for stg layer + - name: col_02 + type: date + description: shipments attribute 2 for stg layer + catalog.int_shipments_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for int layer + - name: col_01 + type: int + description: shipments attribute 1 for int layer + - name: col_02 + type: date + description: shipments attribute 2 for int layer + catalog.mart_shipments_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for mart layer + - name: col_01 + type: int + description: shipments attribute 1 for mart layer + - name: col_02 + type: varchar + description: shipments attribute 2 for mart layer + catalog.raw_carriers_00: + owner_team: catalog + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for raw layer + - name: col_02 + type: date + description: carriers attribute 2 for raw layer + catalog.stg_carriers_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: carriers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for stg layer + - name: col_02 + type: int + description: carriers attribute 2 for stg layer + catalog.int_carriers_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for int layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for int layer + catalog.mart_carriers_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for mart layer + catalog.raw_products_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: products attribute 2 for raw layer + catalog.stg_products_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for stg layer + - name: col_01 + type: int + description: products attribute 1 for stg layer + - name: col_02 + type: timestamp + description: products attribute 2 for stg layer + catalog.int_products_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for int layer + - name: col_02 + type: varchar + description: products attribute 2 for int layer + catalog.mart_products_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for mart layer + - name: col_01 + type: date + description: products attribute 1 for mart layer + - name: col_02 + type: timestamp + description: products attribute 2 for mart layer + catalog.raw_prices_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for raw layer + catalog.stg_prices_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for stg layer + - name: col_01 + type: date + description: prices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: prices attribute 2 for stg layer + catalog.int_prices_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for int layer + - name: col_01 + type: date + description: prices attribute 1 for int layer + - name: col_02 + type: int + description: prices attribute 2 for int layer + catalog.mart_prices_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for mart layer + - name: col_01 + type: int + description: prices attribute 1 for mart layer + - name: col_02 + type: date + description: prices attribute 2 for mart layer + catalog.raw_queries_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: queries attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for raw layer + catalog.stg_queries_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for stg layer + - name: col_01 + type: varchar + description: queries attribute 1 for stg layer + - name: col_02 + type: varchar + description: queries attribute 2 for stg layer + catalog.int_queries_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for int layer + - name: col_02 + type: timestamp + description: queries attribute 2 for int layer + catalog.mart_queries_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for mart layer + - name: col_01 + type: varchar + description: queries attribute 1 for mart layer + - name: col_02 + type: timestamp + description: queries attribute 2 for mart layer + catalog.raw_clicks_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: varchar + description: clicks attribute 2 for raw layer + catalog.stg_clicks_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for stg layer + - name: col_01 + type: timestamp + description: clicks attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for stg layer + catalog.int_clicks_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for int layer + - name: col_01 + type: int + description: clicks attribute 1 for int layer + - name: col_02 + type: int + description: clicks attribute 2 for int layer + catalog.mart_clicks_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for mart layer + - name: col_01 + type: timestamp + description: clicks attribute 1 for mart layer + - name: col_02 + type: date + description: clicks attribute 2 for mart layer + catalog.raw_payouts_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for raw layer + - name: col_01 + type: varchar + description: payouts attribute 1 for raw layer + - name: col_02 + type: varchar + description: payouts attribute 2 for raw layer + catalog.stg_payouts_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + catalog.int_payouts_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for int layer + - name: col_01 + type: date + description: payouts attribute 1 for int layer + - name: col_02 + type: date + description: payouts attribute 2 for int layer + catalog.mart_payouts_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for mart layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + catalog.raw_settlements_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for raw layer + - name: col_01 + type: varchar + description: settlements attribute 1 for raw layer + - name: col_02 + type: int + description: settlements attribute 2 for raw layer + catalog.stg_settlements_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for stg layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for stg layer + catalog.int_settlements_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for int layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for int layer + - name: col_02 + type: varchar + description: settlements attribute 2 for int layer + catalog.mart_settlements_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for mart layer + - name: col_02 + type: varchar + description: settlements attribute 2 for mart layer + catalog.raw_tickets_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for raw layer + - name: col_01 + type: date + description: tickets attribute 1 for raw layer + - name: col_02 + type: date + description: tickets attribute 2 for raw layer + catalog.stg_tickets_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for stg layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for stg layer + - name: col_02 + type: varchar + description: tickets attribute 2 for stg layer + catalog.int_tickets_00: + owner_team: catalog + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for int layer + - name: col_01 + type: date + description: tickets attribute 1 for int layer + - name: col_02 + type: int + description: tickets attribute 2 for int layer + catalog.mart_tickets_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for mart layer + - name: col_01 + type: varchar + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + catalog.raw_csat_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for raw layer + - name: col_02 + type: timestamp + description: csat attribute 2 for raw layer + catalog.stg_csat_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for stg layer + - name: col_02 + type: timestamp + description: csat attribute 2 for stg layer + catalog.int_csat_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for int layer + - name: col_01 + type: timestamp + description: csat attribute 1 for int layer + - name: col_02 + type: date + description: csat attribute 2 for int layer + catalog.mart_csat_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for mart layer + - name: col_01 + type: date + description: csat attribute 1 for mart layer + - name: col_02 + type: date + description: csat attribute 2 for mart layer + catalog.raw_headcount_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for raw layer + - name: col_01 + type: varchar + description: headcount attribute 1 for raw layer + - name: col_02 + type: int + description: headcount attribute 2 for raw layer + catalog.stg_headcount_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for stg layer + - name: col_01 + type: int + description: headcount attribute 1 for stg layer + - name: col_02 + type: varchar + description: headcount attribute 2 for stg layer + catalog.int_headcount_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for int layer + catalog.mart_headcount_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for mart layer + - name: col_01 + type: date + description: headcount attribute 1 for mart layer + - name: col_02 + type: varchar + description: headcount attribute 2 for mart layer + catalog.raw_payroll_00: + owner_team: catalog + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for raw layer + - name: col_01 + type: date + description: payroll attribute 1 for raw layer + - name: col_02 + type: varchar + description: payroll attribute 2 for raw layer + catalog.stg_payroll_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for stg layer + - name: col_01 + type: varchar + description: payroll attribute 1 for stg layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for stg layer + catalog.int_payroll_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for int layer + - name: col_01 + type: int + description: payroll attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for int layer + catalog.mart_payroll_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: payroll attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for mart layer + - name: col_02 + type: int + description: payroll attribute 2 for mart layer + catalog.raw_forecast_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for raw layer + - name: col_02 + type: int + description: forecast attribute 2 for raw layer + catalog.stg_forecast_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for stg layer + - name: col_01 + type: date + description: forecast attribute 1 for stg layer + - name: col_02 + type: varchar + description: forecast attribute 2 for stg layer + catalog.int_forecast_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for int layer + - name: col_01 + type: date + description: forecast attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for int layer + catalog.mart_forecast_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: forecast attribute 0 for mart layer + - name: col_01 + type: varchar + description: forecast attribute 1 for mart layer + - name: col_02 + type: timestamp + description: forecast attribute 2 for mart layer + catalog.raw_returns_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: returns attribute 0 for raw layer + - name: col_01 + type: varchar + description: returns attribute 1 for raw layer + - name: col_02 + type: varchar + description: returns attribute 2 for raw layer + catalog.stg_returns_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for stg layer + - name: col_02 + type: date + description: returns attribute 2 for stg layer + catalog.int_returns_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for int layer + - name: col_01 + type: date + description: returns attribute 1 for int layer + - name: col_02 + type: date + description: returns attribute 2 for int layer + catalog.mart_returns_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for mart layer + - name: col_01 + type: varchar + description: returns attribute 1 for mart layer + - name: col_02 + type: int + description: returns attribute 2 for mart layer + catalog.raw_suppliers_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for raw layer + catalog.stg_suppliers_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for stg layer + catalog.int_suppliers_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for int layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for int layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for int layer + catalog.mart_suppliers_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for mart layer + - name: col_01 + type: date + description: suppliers attribute 1 for mart layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for mart layer + catalog.raw_warehouses_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for raw layer + - name: col_01 + type: int + description: warehouses attribute 1 for raw layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for raw layer + catalog.stg_warehouses_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for stg layer + - name: col_01 + type: date + description: warehouses attribute 1 for stg layer + - name: col_02 + type: int + description: warehouses attribute 2 for stg layer + catalog.int_warehouses_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for int layer + - name: col_01 + type: date + description: warehouses attribute 1 for int layer + - name: col_02 + type: int + description: warehouses attribute 2 for int layer + catalog.mart_warehouses_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for mart layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for mart layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for mart layer + catalog.raw_promotions_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for raw layer + - name: col_02 + type: int + description: promotions attribute 2 for raw layer + catalog.stg_promotions_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + catalog.int_promotions_00: + owner_team: catalog + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for int layer + - name: col_02 + type: varchar + description: promotions attribute 2 for int layer + catalog.mart_promotions_00: + owner_team: catalog + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: promotions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for mart layer + - name: col_02 + type: int + description: promotions attribute 2 for mart layer + catalog.raw_attribution_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: attribution attribute 0 for raw layer + - name: col_01 + type: date + description: attribution attribute 1 for raw layer + - name: col_02 + type: date + description: attribution attribute 2 for raw layer + catalog.stg_attribution_00: + owner_team: catalog + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: attribution attribute 0 for stg layer + - name: col_01 + type: date + description: attribution attribute 1 for stg layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for stg layer + catalog.int_attribution_00: + owner_team: catalog + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for int layer + - name: col_01 + type: int + description: attribution attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for int layer + catalog.mart_attribution_00: + owner_team: catalog + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for mart layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for mart layer + catalog.raw_cohorts_00: + owner_team: catalog + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for raw layer + - name: col_01 + type: int + description: cohorts attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for raw layer + catalog.stg_cohorts_00: + owner_team: catalog + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for stg layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for stg layer + - name: col_02 + type: date + description: cohorts attribute 2 for stg layer + catalog.int_cohorts_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for int layer + - name: col_01 + type: int + description: cohorts attribute 1 for int layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for int layer + catalog.mart_cohorts_00: + owner_team: catalog + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for mart layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for mart layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for mart layer + search.raw_orders_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for raw layer + - name: col_02 + type: date + description: orders attribute 2 for raw layer + search.stg_orders_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for stg layer + - name: col_02 + type: date + description: orders attribute 2 for stg layer + search.int_orders_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: orders attribute 0 for int layer + - name: col_01 + type: date + description: orders attribute 1 for int layer + - name: col_02 + type: varchar + description: orders attribute 2 for int layer + search.mart_orders_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for mart layer + - name: col_01 + type: varchar + description: orders attribute 1 for mart layer + - name: col_02 + type: varchar + description: orders attribute 2 for mart layer + search.raw_invoices_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for raw layer + - name: col_02 + type: date + description: invoices attribute 2 for raw layer + search.stg_invoices_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for stg layer + - name: col_02 + type: varchar + description: invoices attribute 2 for stg layer + search.int_invoices_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for int layer + - name: col_01 + type: date + description: invoices attribute 1 for int layer + - name: col_02 + type: date + description: invoices attribute 2 for int layer + search.mart_invoices_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for mart layer + - name: col_01 + type: date + description: invoices attribute 1 for mart layer + - name: col_02 + type: int + description: invoices attribute 2 for mart layer + search.raw_ledger_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for raw layer + - name: col_01 + type: varchar + description: ledger attribute 1 for raw layer + - name: col_02 + type: date + description: ledger attribute 2 for raw layer + search.stg_ledger_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for stg layer + - name: col_01 + type: varchar + description: ledger attribute 1 for stg layer + - name: col_02 + type: date + description: ledger attribute 2 for stg layer + search.int_ledger_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for int layer + - name: col_01 + type: varchar + description: ledger attribute 1 for int layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for int layer + search.mart_ledger_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: ledger attribute 1 for mart layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for mart layer + search.raw_refunds_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for raw layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for raw layer + - name: col_02 + type: int + description: refunds attribute 2 for raw layer + search.stg_refunds_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for stg layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for stg layer + search.int_refunds_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for int layer + - name: col_01 + type: varchar + description: refunds attribute 1 for int layer + - name: col_02 + type: varchar + description: refunds attribute 2 for int layer + search.mart_refunds_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for mart layer + - name: col_01 + type: varchar + description: refunds attribute 1 for mart layer + - name: col_02 + type: varchar + description: refunds attribute 2 for mart layer + search.raw_sessions_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for raw layer + - name: col_01 + type: int + description: sessions attribute 1 for raw layer + - name: col_02 + type: int + description: sessions attribute 2 for raw layer + search.stg_sessions_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for stg layer + - name: col_02 + type: date + description: sessions attribute 2 for stg layer + search.int_sessions_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for int layer + - name: col_01 + type: date + description: sessions attribute 1 for int layer + - name: col_02 + type: date + description: sessions attribute 2 for int layer + search.mart_sessions_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + search.raw_signups_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for raw layer + - name: col_01 + type: int + description: signups attribute 1 for raw layer + - name: col_02 + type: date + description: signups attribute 2 for raw layer + search.stg_signups_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: signups attribute 0 for stg layer + - name: col_01 + type: varchar + description: signups attribute 1 for stg layer + - name: col_02 + type: varchar + description: signups attribute 2 for stg layer + search.int_signups_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for int layer + - name: col_01 + type: date + description: signups attribute 1 for int layer + - name: col_02 + type: int + description: signups attribute 2 for int layer + search.mart_signups_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: signups attribute 0 for mart layer + - name: col_01 + type: varchar + description: signups attribute 1 for mart layer + - name: col_02 + type: date + description: signups attribute 2 for mart layer + search.raw_campaigns_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for raw layer + - name: col_01 + type: int + description: campaigns attribute 1 for raw layer + - name: col_02 + type: int + description: campaigns attribute 2 for raw layer + search.stg_campaigns_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for stg layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for stg layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for stg layer + search.int_campaigns_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for int layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for int layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for int layer + search.mart_campaigns_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for mart layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for mart layer + search.raw_spend_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for raw layer + - name: col_01 + type: timestamp + description: spend attribute 1 for raw layer + - name: col_02 + type: date + description: spend attribute 2 for raw layer + search.stg_spend_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for stg layer + - name: col_01 + type: timestamp + description: spend attribute 1 for stg layer + - name: col_02 + type: date + description: spend attribute 2 for stg layer + search.int_spend_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for int layer + - name: col_01 + type: timestamp + description: spend attribute 1 for int layer + - name: col_02 + type: varchar + description: spend attribute 2 for int layer + search.mart_spend_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for mart layer + - name: col_01 + type: int + description: spend attribute 1 for mart layer + - name: col_02 + type: varchar + description: spend attribute 2 for mart layer + search.raw_fraud_scores_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for raw layer + search.stg_fraud_scores_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for stg layer + search.int_fraud_scores_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for int layer + search.mart_fraud_scores_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for mart layer + search.raw_chargebacks_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: date + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for raw layer + search.stg_chargebacks_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: int + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for stg layer + search.int_chargebacks_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for int layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for int layer + - name: col_02 + type: int + description: chargebacks attribute 2 for int layer + search.mart_chargebacks_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for mart layer + search.raw_inventory_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for raw layer + - name: col_01 + type: date + description: inventory attribute 1 for raw layer + - name: col_02 + type: int + description: inventory attribute 2 for raw layer + search.stg_inventory_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for stg layer + - name: col_01 + type: int + description: inventory attribute 1 for stg layer + - name: col_02 + type: varchar + description: inventory attribute 2 for stg layer + search.int_inventory_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for int layer + - name: col_01 + type: int + description: inventory attribute 1 for int layer + - name: col_02 + type: varchar + description: inventory attribute 2 for int layer + search.mart_inventory_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for mart layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for mart layer + search.raw_shipments_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: shipments attribute 0 for raw layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for raw layer + - name: col_02 + type: varchar + description: shipments attribute 2 for raw layer + search.stg_shipments_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + search.int_shipments_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for int layer + - name: col_01 + type: date + description: shipments attribute 1 for int layer + - name: col_02 + type: int + description: shipments attribute 2 for int layer + search.mart_shipments_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for mart layer + - name: col_01 + type: int + description: shipments attribute 1 for mart layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for mart layer + search.raw_carriers_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for raw layer + - name: col_01 + type: int + description: carriers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for raw layer + search.stg_carriers_00: + owner_team: search + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: carriers attribute 0 for stg layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for stg layer + search.int_carriers_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for int layer + - name: col_02 + type: varchar + description: carriers attribute 2 for int layer + search.mart_carriers_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: int + description: carriers attribute 2 for mart layer + search.raw_products_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: products attribute 0 for raw layer + - name: col_01 + type: varchar + description: products attribute 1 for raw layer + - name: col_02 + type: varchar + description: products attribute 2 for raw layer + search.stg_products_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: products attribute 0 for stg layer + - name: col_01 + type: date + description: products attribute 1 for stg layer + - name: col_02 + type: int + description: products attribute 2 for stg layer + search.int_products_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for int layer + - name: col_02 + type: timestamp + description: products attribute 2 for int layer + search.mart_products_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for mart layer + - name: col_01 + type: varchar + description: products attribute 1 for mart layer + - name: col_02 + type: timestamp + description: products attribute 2 for mart layer + search.raw_prices_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for raw layer + - name: col_02 + type: date + description: prices attribute 2 for raw layer + search.stg_prices_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for stg layer + - name: col_01 + type: date + description: prices attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for stg layer + search.int_prices_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for int layer + - name: col_01 + type: timestamp + description: prices attribute 1 for int layer + - name: col_02 + type: int + description: prices attribute 2 for int layer + search.mart_prices_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: prices attribute 0 for mart layer + - name: col_01 + type: date + description: prices attribute 1 for mart layer + - name: col_02 + type: timestamp + description: prices attribute 2 for mart layer + search.raw_queries_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for raw layer + - name: col_01 + type: date + description: queries attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: queries attribute 2 for raw layer + search.stg_queries_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for stg layer + - name: col_01 + type: varchar + description: queries attribute 1 for stg layer + - name: col_02 + type: date + description: queries attribute 2 for stg layer + search.int_queries_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for int layer + - name: col_01 + type: varchar + description: queries attribute 1 for int layer + - name: col_02 + type: date + description: queries attribute 2 for int layer + search.mart_queries_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for mart layer + - name: col_01 + type: varchar + description: queries attribute 1 for mart layer + - name: col_02 + type: timestamp + description: queries attribute 2 for mart layer + search.raw_clicks_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for raw layer + - name: col_01 + type: int + description: clicks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for raw layer + search.stg_clicks_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for stg layer + search.int_clicks_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for int layer + - name: col_02 + type: int + description: clicks attribute 2 for int layer + search.mart_clicks_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for mart layer + - name: col_01 + type: varchar + description: clicks attribute 1 for mart layer + - name: col_02 + type: varchar + description: clicks attribute 2 for mart layer + search.raw_payouts_00: + owner_team: search + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: payouts attribute 0 for raw layer + - name: col_01 + type: int + description: payouts attribute 1 for raw layer + - name: col_02 + type: int + description: payouts attribute 2 for raw layer + search.stg_payouts_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: int + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + search.int_payouts_00: + owner_team: search + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for int layer + - name: col_01 + type: int + description: payouts attribute 1 for int layer + - name: col_02 + type: varchar + description: payouts attribute 2 for int layer + search.mart_payouts_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + search.raw_settlements_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for raw layer + - name: col_01 + type: varchar + description: settlements attribute 1 for raw layer + - name: col_02 + type: int + description: settlements attribute 2 for raw layer + search.stg_settlements_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for stg layer + - name: col_01 + type: varchar + description: settlements attribute 1 for stg layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for stg layer + search.int_settlements_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for int layer + - name: col_02 + type: int + description: settlements attribute 2 for int layer + search.mart_settlements_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for mart layer + - name: col_01 + type: date + description: settlements attribute 1 for mart layer + - name: col_02 + type: varchar + description: settlements attribute 2 for mart layer + search.raw_tickets_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for raw layer + - name: col_01 + type: date + description: tickets attribute 1 for raw layer + - name: col_02 + type: varchar + description: tickets attribute 2 for raw layer + search.stg_tickets_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for stg layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for stg layer + search.int_tickets_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for int layer + - name: col_02 + type: date + description: tickets attribute 2 for int layer + search.mart_tickets_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for mart layer + - name: col_01 + type: timestamp + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + search.raw_csat_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: csat attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for raw layer + - name: col_02 + type: date + description: csat attribute 2 for raw layer + search.stg_csat_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for stg layer + - name: col_01 + type: date + description: csat attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for stg layer + search.int_csat_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for int layer + - name: col_01 + type: date + description: csat attribute 1 for int layer + - name: col_02 + type: timestamp + description: csat attribute 2 for int layer + search.mart_csat_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for mart layer + - name: col_01 + type: varchar + description: csat attribute 1 for mart layer + - name: col_02 + type: date + description: csat attribute 2 for mart layer + search.raw_headcount_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: headcount attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for raw layer + - name: col_02 + type: varchar + description: headcount attribute 2 for raw layer + search.stg_headcount_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for stg layer + - name: col_02 + type: date + description: headcount attribute 2 for stg layer + search.int_headcount_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for int layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for int layer + - name: col_02 + type: date + description: headcount attribute 2 for int layer + search.mart_headcount_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: headcount attribute 0 for mart layer + - name: col_01 + type: int + description: headcount attribute 1 for mart layer + - name: col_02 + type: date + description: headcount attribute 2 for mart layer + search.raw_payroll_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for raw layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for raw layer + - name: col_02 + type: date + description: payroll attribute 2 for raw layer + search.stg_payroll_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: payroll attribute 0 for stg layer + - name: col_01 + type: int + description: payroll attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for stg layer + search.int_payroll_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for int layer + - name: col_01 + type: int + description: payroll attribute 1 for int layer + - name: col_02 + type: date + description: payroll attribute 2 for int layer + search.mart_payroll_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for mart layer + - name: col_01 + type: int + description: payroll attribute 1 for mart layer + - name: col_02 + type: int + description: payroll attribute 2 for mart layer + search.raw_forecast_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for raw layer + - name: col_01 + type: int + description: forecast attribute 1 for raw layer + - name: col_02 + type: int + description: forecast attribute 2 for raw layer + search.stg_forecast_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: forecast attribute 0 for stg layer + - name: col_01 + type: int + description: forecast attribute 1 for stg layer + - name: col_02 + type: date + description: forecast attribute 2 for stg layer + search.int_forecast_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for int layer + - name: col_01 + type: varchar + description: forecast attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for int layer + search.mart_forecast_00: + owner_team: search + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for mart layer + search.raw_returns_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for raw layer + search.stg_returns_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: returns attribute 0 for stg layer + - name: col_01 + type: int + description: returns attribute 1 for stg layer + - name: col_02 + type: int + description: returns attribute 2 for stg layer + search.int_returns_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: returns attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for int layer + - name: col_02 + type: date + description: returns attribute 2 for int layer + search.mart_returns_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for mart layer + - name: col_01 + type: varchar + description: returns attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for mart layer + search.raw_suppliers_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for raw layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for raw layer + search.stg_suppliers_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for stg layer + - name: col_01 + type: date + description: suppliers attribute 1 for stg layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for stg layer + search.int_suppliers_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for int layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for int layer + - name: col_02 + type: date + description: suppliers attribute 2 for int layer + search.mart_suppliers_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: int + description: suppliers attribute 1 for mart layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for mart layer + search.raw_warehouses_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for raw layer + - name: col_01 + type: date + description: warehouses attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for raw layer + search.stg_warehouses_00: + owner_team: search + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for stg layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for stg layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for stg layer + search.int_warehouses_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for int layer + - name: col_01 + type: int + description: warehouses attribute 1 for int layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for int layer + search.mart_warehouses_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for mart layer + - name: col_01 + type: int + description: warehouses attribute 1 for mart layer + - name: col_02 + type: date + description: warehouses attribute 2 for mart layer + search.raw_promotions_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for raw layer + - name: col_01 + type: date + description: promotions attribute 1 for raw layer + - name: col_02 + type: int + description: promotions attribute 2 for raw layer + search.stg_promotions_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for stg layer + - name: col_01 + type: int + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + search.int_promotions_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for int layer + - name: col_01 + type: varchar + description: promotions attribute 1 for int layer + - name: col_02 + type: date + description: promotions attribute 2 for int layer + search.mart_promotions_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for mart layer + search.raw_attribution_00: + owner_team: search + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for raw layer + - name: col_01 + type: varchar + description: attribution attribute 1 for raw layer + - name: col_02 + type: int + description: attribution attribute 2 for raw layer + search.stg_attribution_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: attribution attribute 0 for stg layer + - name: col_01 + type: int + description: attribution attribute 1 for stg layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for stg layer + search.int_attribution_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for int layer + - name: col_01 + type: varchar + description: attribution attribute 1 for int layer + - name: col_02 + type: timestamp + description: attribution attribute 2 for int layer + search.mart_attribution_00: + owner_team: search + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for mart layer + search.raw_cohorts_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for raw layer + - name: col_01 + type: date + description: cohorts attribute 1 for raw layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for raw layer + search.stg_cohorts_00: + owner_team: search + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for stg layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for stg layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for stg layer + search.int_cohorts_00: + owner_team: search + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for int layer + - name: col_01 + type: date + description: cohorts attribute 1 for int layer + - name: col_02 + type: date + description: cohorts attribute 2 for int layer + search.mart_cohorts_00: + owner_team: search + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for mart layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for mart layer + payments.raw_orders_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for raw layer + - name: col_01 + type: date + description: orders attribute 1 for raw layer + - name: col_02 + type: int + description: orders attribute 2 for raw layer + payments.stg_orders_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for stg layer + - name: col_01 + type: timestamp + description: orders attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for stg layer + payments.int_orders_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for int layer + - name: col_01 + type: date + description: orders attribute 1 for int layer + - name: col_02 + type: int + description: orders attribute 2 for int layer + payments.mart_orders_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: orders attribute 0 for mart layer + - name: col_01 + type: varchar + description: orders attribute 1 for mart layer + - name: col_02 + type: int + description: orders attribute 2 for mart layer + payments.raw_invoices_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for raw layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for raw layer + payments.stg_invoices_00: + owner_team: payments + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for stg layer + - name: col_01 + type: date + description: invoices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for stg layer + payments.int_invoices_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for int layer + payments.mart_invoices_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for mart layer + - name: col_01 + type: int + description: invoices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for mart layer + payments.raw_ledger_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for raw layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for raw layer + - name: col_02 + type: int + description: ledger attribute 2 for raw layer + payments.stg_ledger_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for stg layer + - name: col_01 + type: int + description: ledger attribute 1 for stg layer + - name: col_02 + type: int + description: ledger attribute 2 for stg layer + payments.int_ledger_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for int layer + - name: col_01 + type: date + description: ledger attribute 1 for int layer + - name: col_02 + type: int + description: ledger attribute 2 for int layer + payments.mart_ledger_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for mart layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for mart layer + - name: col_02 + type: int + description: ledger attribute 2 for mart layer + payments.raw_refunds_00: + owner_team: payments + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for raw layer + - name: col_02 + type: varchar + description: refunds attribute 2 for raw layer + payments.stg_refunds_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for stg layer + payments.int_refunds_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for int layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: refunds attribute 2 for int layer + payments.mart_refunds_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for mart layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for mart layer + - name: col_02 + type: int + description: refunds attribute 2 for mart layer + payments.raw_sessions_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: int + description: sessions attribute 2 for raw layer + payments.stg_sessions_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for stg layer + - name: col_01 + type: int + description: sessions attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for stg layer + payments.int_sessions_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: sessions attribute 0 for int layer + - name: col_01 + type: date + description: sessions attribute 1 for int layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for int layer + payments.mart_sessions_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for mart layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for mart layer + payments.raw_signups_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: signups attribute 0 for raw layer + - name: col_01 + type: date + description: signups attribute 1 for raw layer + - name: col_02 + type: timestamp + description: signups attribute 2 for raw layer + payments.stg_signups_00: + owner_team: payments + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: signups attribute 0 for stg layer + - name: col_01 + type: varchar + description: signups attribute 1 for stg layer + - name: col_02 + type: date + description: signups attribute 2 for stg layer + payments.int_signups_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for int layer + - name: col_01 + type: varchar + description: signups attribute 1 for int layer + - name: col_02 + type: varchar + description: signups attribute 2 for int layer + payments.mart_signups_00: + owner_team: payments + retention_days: 30 + pii: true + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for mart layer + - name: col_02 + type: timestamp + description: signups attribute 2 for mart layer + payments.raw_campaigns_00: + owner_team: payments + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for raw layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + payments.stg_campaigns_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for stg layer + - name: col_01 + type: date + description: campaigns attribute 1 for stg layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for stg layer + payments.int_campaigns_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for int layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for int layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for int layer + payments.mart_campaigns_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for mart layer + - name: col_01 + type: int + description: campaigns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for mart layer + payments.raw_spend_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for raw layer + - name: col_01 + type: int + description: spend attribute 1 for raw layer + - name: col_02 + type: timestamp + description: spend attribute 2 for raw layer + payments.stg_spend_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for stg layer + - name: col_01 + type: varchar + description: spend attribute 1 for stg layer + - name: col_02 + type: int + description: spend attribute 2 for stg layer + payments.int_spend_00: + owner_team: payments + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: int + description: spend attribute 1 for int layer + - name: col_02 + type: timestamp + description: spend attribute 2 for int layer + payments.mart_spend_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: spend attribute 0 for mart layer + - name: col_01 + type: varchar + description: spend attribute 1 for mart layer + - name: col_02 + type: date + description: spend attribute 2 for mart layer + payments.raw_fraud_scores_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for raw layer + payments.stg_fraud_scores_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: varchar + description: fraud_scores attribute 2 for stg layer + payments.int_fraud_scores_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for int layer + payments.mart_fraud_scores_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for mart layer + payments.raw_chargebacks_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: date + description: chargebacks attribute 2 for raw layer + payments.stg_chargebacks_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for stg layer + payments.int_chargebacks_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: chargebacks attribute 1 for int layer + - name: col_02 + type: varchar + description: chargebacks attribute 2 for int layer + payments.mart_chargebacks_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: date + description: chargebacks attribute 2 for mart layer + payments.raw_inventory_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for raw layer + - name: col_01 + type: timestamp + description: inventory attribute 1 for raw layer + - name: col_02 + type: date + description: inventory attribute 2 for raw layer + payments.stg_inventory_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for stg layer + payments.int_inventory_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for int layer + - name: col_02 + type: date + description: inventory attribute 2 for int layer + payments.mart_inventory_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for mart layer + - name: col_01 + type: int + description: inventory attribute 1 for mart layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for mart layer + payments.raw_shipments_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for raw layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for raw layer + - name: col_02 + type: int + description: shipments attribute 2 for raw layer + payments.stg_shipments_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for stg layer + - name: col_01 + type: varchar + description: shipments attribute 1 for stg layer + - name: col_02 + type: varchar + description: shipments attribute 2 for stg layer + payments.int_shipments_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for int layer + - name: col_01 + type: varchar + description: shipments attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for int layer + payments.mart_shipments_00: + owner_team: payments + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: shipments attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for mart layer + payments.raw_carriers_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for raw layer + - name: col_01 + type: int + description: carriers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for raw layer + payments.stg_carriers_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for stg layer + - name: col_01 + type: date + description: carriers attribute 1 for stg layer + - name: col_02 + type: int + description: carriers attribute 2 for stg layer + payments.int_carriers_00: + owner_team: payments + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for int layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for int layer + - name: col_02 + type: int + description: carriers attribute 2 for int layer + payments.mart_carriers_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for mart layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for mart layer + - name: col_02 + type: int + description: carriers attribute 2 for mart layer + payments.raw_products_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for raw layer + - name: col_01 + type: int + description: products attribute 1 for raw layer + - name: col_02 + type: timestamp + description: products attribute 2 for raw layer + payments.stg_products_00: + owner_team: payments + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: products attribute 0 for stg layer + - name: col_01 + type: date + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + payments.int_products_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for int layer + - name: col_01 + type: timestamp + description: products attribute 1 for int layer + - name: col_02 + type: int + description: products attribute 2 for int layer + payments.mart_products_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for mart layer + - name: col_01 + type: varchar + description: products attribute 1 for mart layer + - name: col_02 + type: varchar + description: products attribute 2 for mart layer + payments.raw_prices_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for raw layer + - name: col_01 + type: timestamp + description: prices attribute 1 for raw layer + - name: col_02 + type: int + description: prices attribute 2 for raw layer + payments.stg_prices_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: prices attribute 0 for stg layer + - name: col_01 + type: varchar + description: prices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: prices attribute 2 for stg layer + payments.int_prices_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for int layer + - name: col_01 + type: date + description: prices attribute 1 for int layer + - name: col_02 + type: date + description: prices attribute 2 for int layer + payments.mart_prices_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: prices attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: prices attribute 1 for mart layer + - name: col_02 + type: date + description: prices attribute 2 for mart layer + payments.raw_queries_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for raw layer + - name: col_01 + type: int + description: queries attribute 1 for raw layer + - name: col_02 + type: date + description: queries attribute 2 for raw layer + payments.stg_queries_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for stg layer + - name: col_01 + type: timestamp + description: queries attribute 1 for stg layer + - name: col_02 + type: date + description: queries attribute 2 for stg layer + payments.int_queries_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: queries attribute 0 for int layer + - name: col_01 + type: int + description: queries attribute 1 for int layer + - name: col_02 + type: int + description: queries attribute 2 for int layer + payments.mart_queries_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: queries attribute 1 for mart layer + - name: col_02 + type: varchar + description: queries attribute 2 for mart layer + payments.raw_clicks_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for raw layer + - name: col_02 + type: int + description: clicks attribute 2 for raw layer + payments.stg_clicks_00: + owner_team: payments + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: varchar + description: clicks attribute 2 for stg layer + payments.int_clicks_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for int layer + - name: col_01 + type: varchar + description: clicks attribute 1 for int layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for int layer + payments.mart_clicks_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for mart layer + - name: col_02 + type: varchar + description: clicks attribute 2 for mart layer + payments.raw_payouts_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for raw layer + - name: col_02 + type: int + description: payouts attribute 2 for raw layer + payments.stg_payouts_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for stg layer + - name: col_02 + type: varchar + description: payouts attribute 2 for stg layer + payments.int_payouts_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: payouts attribute 0 for int layer + - name: col_01 + type: varchar + description: payouts attribute 1 for int layer + - name: col_02 + type: int + description: payouts attribute 2 for int layer + payments.mart_payouts_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for mart layer + - name: col_01 + type: varchar + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + payments.raw_settlements_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for raw layer + - name: col_01 + type: int + description: settlements attribute 1 for raw layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for raw layer + payments.stg_settlements_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for stg layer + - name: col_01 + type: date + description: settlements attribute 1 for stg layer + - name: col_02 + type: int + description: settlements attribute 2 for stg layer + payments.int_settlements_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for int layer + - name: col_01 + type: varchar + description: settlements attribute 1 for int layer + - name: col_02 + type: varchar + description: settlements attribute 2 for int layer + payments.mart_settlements_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: settlements attribute 0 for mart layer + - name: col_01 + type: varchar + description: settlements attribute 1 for mart layer + - name: col_02 + type: int + description: settlements attribute 2 for mart layer + payments.raw_tickets_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for raw layer + - name: col_01 + type: varchar + description: tickets attribute 1 for raw layer + - name: col_02 + type: date + description: tickets attribute 2 for raw layer + payments.stg_tickets_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for stg layer + - name: col_01 + type: date + description: tickets attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for stg layer + payments.int_tickets_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for int layer + - name: col_01 + type: int + description: tickets attribute 1 for int layer + - name: col_02 + type: date + description: tickets attribute 2 for int layer + payments.mart_tickets_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for mart layer + - name: col_01 + type: int + description: tickets attribute 1 for mart layer + - name: col_02 + type: int + description: tickets attribute 2 for mart layer + payments.raw_csat_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for raw layer + - name: col_01 + type: varchar + description: csat attribute 1 for raw layer + - name: col_02 + type: int + description: csat attribute 2 for raw layer + payments.stg_csat_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: csat attribute 0 for stg layer + - name: col_01 + type: int + description: csat attribute 1 for stg layer + - name: col_02 + type: int + description: csat attribute 2 for stg layer + payments.int_csat_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: csat attribute 0 for int layer + - name: col_01 + type: varchar + description: csat attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for int layer + payments.mart_csat_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for mart layer + - name: col_01 + type: varchar + description: csat attribute 1 for mart layer + - name: col_02 + type: timestamp + description: csat attribute 2 for mart layer + payments.raw_headcount_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for raw layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for raw layer + - name: col_02 + type: date + description: headcount attribute 2 for raw layer + payments.stg_headcount_00: + owner_team: payments + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: headcount attribute 0 for stg layer + - name: col_01 + type: int + description: headcount attribute 1 for stg layer + - name: col_02 + type: varchar + description: headcount attribute 2 for stg layer + payments.int_headcount_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for int layer + - name: col_01 + type: int + description: headcount attribute 1 for int layer + - name: col_02 + type: int + description: headcount attribute 2 for int layer + payments.mart_headcount_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: headcount attribute 0 for mart layer + - name: col_01 + type: varchar + description: headcount attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: headcount attribute 2 for mart layer + payments.raw_payroll_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for raw layer + - name: col_01 + type: date + description: payroll attribute 1 for raw layer + - name: col_02 + type: int + description: payroll attribute 2 for raw layer + payments.stg_payroll_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for stg layer + - name: col_01 + type: date + description: payroll attribute 1 for stg layer + - name: col_02 + type: varchar + description: payroll attribute 2 for stg layer + payments.int_payroll_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payroll attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for int layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for int layer + payments.mart_payroll_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: payroll attribute 0 for mart layer + - name: col_01 + type: date + description: payroll attribute 1 for mart layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for mart layer + payments.raw_forecast_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for raw layer + - name: col_02 + type: varchar + description: forecast attribute 2 for raw layer + payments.stg_forecast_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: forecast attribute 0 for stg layer + - name: col_01 + type: int + description: forecast attribute 1 for stg layer + - name: col_02 + type: date + description: forecast attribute 2 for stg layer + payments.int_forecast_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for int layer + - name: col_01 + type: date + description: forecast attribute 1 for int layer + - name: col_02 + type: timestamp + description: forecast attribute 2 for int layer + payments.mart_forecast_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for mart layer + - name: col_01 + type: varchar + description: forecast attribute 1 for mart layer + - name: col_02 + type: date + description: forecast attribute 2 for mart layer + payments.raw_returns_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: timestamp + description: returns attribute 2 for raw layer + payments.stg_returns_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for stg layer + - name: col_01 + type: varchar + description: returns attribute 1 for stg layer + - name: col_02 + type: varchar + description: returns attribute 2 for stg layer + payments.int_returns_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for int layer + - name: col_01 + type: timestamp + description: returns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for int layer + payments.mart_returns_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: returns attribute 0 for mart layer + - name: col_01 + type: date + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + payments.raw_suppliers_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: suppliers attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for raw layer + - name: col_02 + type: int + description: suppliers attribute 2 for raw layer + payments.stg_suppliers_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for stg layer + - name: col_01 + type: date + description: suppliers attribute 1 for stg layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for stg layer + payments.int_suppliers_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for int layer + - name: col_01 + type: date + description: suppliers attribute 1 for int layer + - name: col_02 + type: int + description: suppliers attribute 2 for int layer + payments.mart_suppliers_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: varchar + description: suppliers attribute 1 for mart layer + - name: col_02 + type: int + description: suppliers attribute 2 for mart layer + payments.raw_warehouses_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for raw layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for raw layer + - name: col_02 + type: int + description: warehouses attribute 2 for raw layer + payments.stg_warehouses_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for stg layer + - name: col_01 + type: int + description: warehouses attribute 1 for stg layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for stg layer + payments.int_warehouses_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: warehouses attribute 0 for int layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for int layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for int layer + payments.mart_warehouses_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for mart layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for mart layer + - name: col_02 + type: varchar + description: warehouses attribute 2 for mart layer + payments.raw_promotions_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for raw layer + - name: col_01 + type: varchar + description: promotions attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for raw layer + payments.stg_promotions_00: + owner_team: payments + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for stg layer + - name: col_01 + type: varchar + description: promotions attribute 1 for stg layer + - name: col_02 + type: date + description: promotions attribute 2 for stg layer + payments.int_promotions_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: promotions attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for int layer + payments.mart_promotions_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: promotions attribute 0 for mart layer + - name: col_01 + type: date + description: promotions attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: promotions attribute 2 for mart layer + payments.raw_attribution_00: + owner_team: payments + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for raw layer + - name: col_01 + type: int + description: attribution attribute 1 for raw layer + - name: col_02 + type: int + description: attribution attribute 2 for raw layer + payments.stg_attribution_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for stg layer + - name: col_01 + type: int + description: attribution attribute 1 for stg layer + - name: col_02 + type: date + description: attribution attribute 2 for stg layer + payments.int_attribution_00: + owner_team: payments + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for int layer + payments.mart_attribution_00: + owner_team: payments + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: attribution attribute 2 for mart layer + payments.raw_cohorts_00: + owner_team: payments + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: cohorts attribute 1 for raw layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for raw layer + payments.stg_cohorts_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for stg layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for stg layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for stg layer + payments.int_cohorts_00: + owner_team: payments + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for int layer + - name: col_01 + type: date + description: cohorts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: cohorts attribute 2 for int layer + payments.mart_cohorts_00: + owner_team: payments + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for mart layer + - name: col_01 + type: date + description: cohorts attribute 1 for mart layer + - name: col_02 + type: date + description: cohorts attribute 2 for mart layer + support.raw_orders_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: orders attribute 1 for raw layer + - name: col_02 + type: date + description: orders attribute 2 for raw layer + support.stg_orders_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: orders attribute 0 for stg layer + - name: col_01 + type: varchar + description: orders attribute 1 for stg layer + - name: col_02 + type: timestamp + description: orders attribute 2 for stg layer + support.int_orders_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: orders attribute 0 for int layer + - name: col_01 + type: timestamp + description: orders attribute 1 for int layer + - name: col_02 + type: int + description: orders attribute 2 for int layer + support.mart_orders_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for mart layer + - name: col_01 + type: date + description: orders attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: orders attribute 2 for mart layer + support.raw_invoices_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for raw layer + - name: col_01 + type: varchar + description: invoices attribute 1 for raw layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for raw layer + support.stg_invoices_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for stg layer + - name: col_01 + type: varchar + description: invoices attribute 1 for stg layer + - name: col_02 + type: timestamp + description: invoices attribute 2 for stg layer + support.int_invoices_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for int layer + support.mart_invoices_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: invoices attribute 0 for mart layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for mart layer + - name: col_02 + type: varchar + description: invoices attribute 2 for mart layer + support.raw_ledger_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for raw layer + - name: col_01 + type: varchar + description: ledger attribute 1 for raw layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for raw layer + support.stg_ledger_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: ledger attribute 0 for stg layer + - name: col_01 + type: varchar + description: ledger attribute 1 for stg layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for stg layer + support.int_ledger_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: ledger attribute 0 for int layer + - name: col_01 + type: date + description: ledger attribute 1 for int layer + - name: col_02 + type: varchar + description: ledger attribute 2 for int layer + support.mart_ledger_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: ledger attribute 0 for mart layer + - name: col_01 + type: date + description: ledger attribute 1 for mart layer + - name: col_02 + type: varchar + description: ledger attribute 2 for mart layer + support.raw_refunds_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for raw layer + - name: col_01 + type: date + description: refunds attribute 1 for raw layer + - name: col_02 + type: date + description: refunds attribute 2 for raw layer + support.stg_refunds_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for stg layer + - name: col_01 + type: varchar + description: refunds attribute 1 for stg layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for stg layer + support.int_refunds_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: refunds attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: refunds attribute 1 for int layer + - name: col_02 + type: date + description: refunds attribute 2 for int layer + support.mart_refunds_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: refunds attribute 0 for mart layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for mart layer + - name: col_02 + type: int + description: refunds attribute 2 for mart layer + support.raw_sessions_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: sessions attribute 1 for raw layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for raw layer + support.stg_sessions_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for stg layer + - name: col_01 + type: varchar + description: sessions attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: sessions attribute 2 for stg layer + support.int_sessions_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: sessions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for int layer + - name: col_02 + type: date + description: sessions attribute 2 for int layer + support.mart_sessions_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for mart layer + - name: col_02 + type: date + description: sessions attribute 2 for mart layer + support.raw_signups_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for raw layer + - name: col_01 + type: timestamp + description: signups attribute 1 for raw layer + - name: col_02 + type: date + description: signups attribute 2 for raw layer + support.stg_signups_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for stg layer + - name: col_01 + type: timestamp + description: signups attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for stg layer + support.int_signups_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: signups attribute 1 for int layer + - name: col_02 + type: date + description: signups attribute 2 for int layer + support.mart_signups_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: signups attribute 0 for mart layer + - name: col_01 + type: timestamp + description: signups attribute 1 for mart layer + - name: col_02 + type: varchar + description: signups attribute 2 for mart layer + support.raw_campaigns_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + support.stg_campaigns_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: campaigns attribute 0 for stg layer + - name: col_01 + type: int + description: campaigns attribute 1 for stg layer + - name: col_02 + type: date + description: campaigns attribute 2 for stg layer + support.int_campaigns_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for int layer + - name: col_01 + type: timestamp + description: campaigns attribute 1 for int layer + - name: col_02 + type: int + description: campaigns attribute 2 for int layer + support.mart_campaigns_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: campaigns attribute 0 for mart layer + - name: col_01 + type: int + description: campaigns attribute 1 for mart layer + - name: col_02 + type: int + description: campaigns attribute 2 for mart layer + support.raw_spend_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: spend attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for raw layer + - name: col_02 + type: varchar + description: spend attribute 2 for raw layer + support.stg_spend_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: spend attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: spend attribute 1 for stg layer + - name: col_02 + type: int + description: spend attribute 2 for stg layer + support.int_spend_00: + owner_team: support + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: date + description: spend attribute 1 for int layer + - name: col_02 + type: int + description: spend attribute 2 for int layer + support.mart_spend_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for mart layer + - name: col_01 + type: timestamp + description: spend attribute 1 for mart layer + - name: col_02 + type: timestamp + description: spend attribute 2 for mart layer + support.raw_fraud_scores_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for raw layer + support.stg_fraud_scores_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for stg layer + support.int_fraud_scores_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for int layer + support.mart_fraud_scores_00: + owner_team: support + retention_days: 365 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: date + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for mart layer + support.raw_chargebacks_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: int + description: chargebacks attribute 2 for raw layer + support.stg_chargebacks_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: int + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: int + description: chargebacks attribute 2 for stg layer + support.int_chargebacks_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for int layer + - name: col_01 + type: int + description: chargebacks attribute 1 for int layer + - name: col_02 + type: timestamp + description: chargebacks attribute 2 for int layer + support.mart_chargebacks_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for mart layer + support.raw_inventory_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: inventory attribute 0 for raw layer + - name: col_01 + type: date + description: inventory attribute 1 for raw layer + - name: col_02 + type: int + description: inventory attribute 2 for raw layer + support.stg_inventory_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: inventory attribute 0 for stg layer + - name: col_01 + type: date + description: inventory attribute 1 for stg layer + - name: col_02 + type: varchar + description: inventory attribute 2 for stg layer + support.int_inventory_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for int layer + - name: col_01 + type: int + description: inventory attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: inventory attribute 2 for int layer + support.mart_inventory_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for mart layer + - name: col_01 + type: int + description: inventory attribute 1 for mart layer + - name: col_02 + type: int + description: inventory attribute 2 for mart layer + support.raw_shipments_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: shipments attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for raw layer + support.stg_shipments_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for stg layer + - name: col_01 + type: date + description: shipments attribute 1 for stg layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for stg layer + support.int_shipments_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: shipments attribute 0 for int layer + - name: col_01 + type: varchar + description: shipments attribute 1 for int layer + - name: col_02 + type: timestamp + description: shipments attribute 2 for int layer + support.mart_shipments_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for mart layer + - name: col_01 + type: date + description: shipments attribute 1 for mart layer + - name: col_02 + type: date + description: shipments attribute 2 for mart layer + support.raw_carriers_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: carriers attribute 0 for raw layer + - name: col_01 + type: timestamp + description: carriers attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: carriers attribute 2 for raw layer + support.stg_carriers_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for stg layer + - name: col_02 + type: timestamp + description: carriers attribute 2 for stg layer + support.int_carriers_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: carriers attribute 1 for int layer + - name: col_02 + type: date + description: carriers attribute 2 for int layer + support.mart_carriers_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: carriers attribute 0 for mart layer + - name: col_01 + type: date + description: carriers attribute 1 for mart layer + - name: col_02 + type: date + description: carriers attribute 2 for mart layer + support.raw_products_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for raw layer + - name: col_01 + type: int + description: products attribute 1 for raw layer + - name: col_02 + type: int + description: products attribute 2 for raw layer + support.stg_products_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for stg layer + - name: col_01 + type: timestamp + description: products attribute 1 for stg layer + - name: col_02 + type: varchar + description: products attribute 2 for stg layer + support.int_products_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: products attribute 0 for int layer + - name: col_01 + type: varchar + description: products attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: products attribute 2 for int layer + support.mart_products_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for mart layer + - name: col_01 + type: timestamp + description: products attribute 1 for mart layer + - name: col_02 + type: date + description: products attribute 2 for mart layer + support.raw_prices_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: varchar + description: prices attribute 2 for raw layer + support.stg_prices_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for stg layer + - name: col_01 + type: varchar + description: prices attribute 1 for stg layer + - name: col_02 + type: varchar + description: prices attribute 2 for stg layer + support.int_prices_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for int layer + - name: col_01 + type: timestamp + description: prices attribute 1 for int layer + - name: col_02 + type: int + description: prices attribute 2 for int layer + support.mart_prices_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for mart layer + - name: col_01 + type: date + description: prices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for mart layer + support.raw_queries_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: queries attribute 0 for raw layer + - name: col_01 + type: int + description: queries attribute 1 for raw layer + - name: col_02 + type: int + description: queries attribute 2 for raw layer + support.stg_queries_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: queries attribute 0 for stg layer + - name: col_01 + type: varchar + description: queries attribute 1 for stg layer + - name: col_02 + type: timestamp + description: queries attribute 2 for stg layer + support.int_queries_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: queries attribute 0 for int layer + - name: col_01 + type: varchar + description: queries attribute 1 for int layer + - name: col_02 + type: varchar + description: queries attribute 2 for int layer + support.mart_queries_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: queries attribute 0 for mart layer + - name: col_01 + type: int + description: queries attribute 1 for mart layer + - name: col_02 + type: timestamp + description: queries attribute 2 for mart layer + support.raw_clicks_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: clicks attribute 0 for raw layer + - name: col_01 + type: int + description: clicks attribute 1 for raw layer + - name: col_02 + type: date + description: clicks attribute 2 for raw layer + support.stg_clicks_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: date + description: clicks attribute 2 for stg layer + support.int_clicks_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for int layer + - name: col_02 + type: varchar + description: clicks attribute 2 for int layer + support.mart_clicks_00: + owner_team: support + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for mart layer + support.raw_payouts_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: payouts attribute 0 for raw layer + - name: col_01 + type: date + description: payouts attribute 1 for raw layer + - name: col_02 + type: int + description: payouts attribute 2 for raw layer + support.stg_payouts_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: payouts attribute 0 for stg layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for stg layer + support.int_payouts_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for int layer + support.mart_payouts_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for mart layer + - name: col_01 + type: varchar + description: payouts attribute 1 for mart layer + - name: col_02 + type: date + description: payouts attribute 2 for mart layer + support.raw_settlements_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: settlements attribute 0 for raw layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for raw layer + support.stg_settlements_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for stg layer + - name: col_01 + type: varchar + description: settlements attribute 1 for stg layer + - name: col_02 + type: varchar + description: settlements attribute 2 for stg layer + support.int_settlements_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: settlements attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: settlements attribute 2 for int layer + support.mart_settlements_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for mart layer + - name: col_01 + type: varchar + description: settlements attribute 1 for mart layer + - name: col_02 + type: date + description: settlements attribute 2 for mart layer + support.raw_tickets_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for raw layer + - name: col_02 + type: int + description: tickets attribute 2 for raw layer + support.stg_tickets_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for stg layer + - name: col_01 + type: int + description: tickets attribute 1 for stg layer + - name: col_02 + type: varchar + description: tickets attribute 2 for stg layer + support.int_tickets_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for int layer + - name: col_02 + type: varchar + description: tickets attribute 2 for int layer + support.mart_tickets_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for mart layer + - name: col_01 + type: date + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + support.raw_csat_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for raw layer + - name: col_01 + type: date + description: csat attribute 1 for raw layer + - name: col_02 + type: int + description: csat attribute 2 for raw layer + support.stg_csat_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: csat attribute 0 for stg layer + - name: col_01 + type: varchar + description: csat attribute 1 for stg layer + - name: col_02 + type: timestamp + description: csat attribute 2 for stg layer + support.int_csat_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: csat attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: csat attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for int layer + support.mart_csat_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: csat attribute 0 for mart layer + - name: col_01 + type: timestamp + description: csat attribute 1 for mart layer + - name: col_02 + type: varchar + description: csat attribute 2 for mart layer + support.raw_headcount_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for raw layer + - name: col_01 + type: int + description: headcount attribute 1 for raw layer + - name: col_02 + type: date + description: headcount attribute 2 for raw layer + support.stg_headcount_00: + owner_team: support + retention_days: 365 + pii: true + columns: + - name: col_00 + type: date + description: headcount attribute 0 for stg layer + - name: col_01 + type: int + description: headcount attribute 1 for stg layer + - name: col_02 + type: int + description: headcount attribute 2 for stg layer + support.int_headcount_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for int layer + - name: col_01 + type: int + description: headcount attribute 1 for int layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for int layer + support.mart_headcount_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for mart layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for mart layer + support.raw_payroll_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: payroll attribute 1 for raw layer + - name: col_02 + type: timestamp + description: payroll attribute 2 for raw layer + support.stg_payroll_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: payroll attribute 0 for stg layer + - name: col_01 + type: timestamp + description: payroll attribute 1 for stg layer + - name: col_02 + type: int + description: payroll attribute 2 for stg layer + support.int_payroll_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: date + description: payroll attribute 1 for int layer + - name: col_02 + type: int + description: payroll attribute 2 for int layer + support.mart_payroll_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: payroll attribute 0 for mart layer + - name: col_01 + type: date + description: payroll attribute 1 for mart layer + - name: col_02 + type: int + description: payroll attribute 2 for mart layer + support.raw_forecast_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for raw layer + - name: col_01 + type: date + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + support.stg_forecast_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for stg layer + - name: col_01 + type: date + description: forecast attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for stg layer + support.int_forecast_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: forecast attribute 0 for int layer + - name: col_01 + type: int + description: forecast attribute 1 for int layer + - name: col_02 + type: date + description: forecast attribute 2 for int layer + support.mart_forecast_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for mart layer + - name: col_01 + type: date + description: forecast attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: forecast attribute 2 for mart layer + support.raw_returns_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for raw layer + - name: col_01 + type: int + description: returns attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for raw layer + support.stg_returns_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: returns attribute 1 for stg layer + - name: col_02 + type: varchar + description: returns attribute 2 for stg layer + support.int_returns_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: int + description: returns attribute 0 for int layer + - name: col_01 + type: timestamp + description: returns attribute 1 for int layer + - name: col_02 + type: varchar + description: returns attribute 2 for int layer + support.mart_returns_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: returns attribute 0 for mart layer + - name: col_01 + type: int + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + support.raw_suppliers_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for raw layer + - name: col_01 + type: date + description: suppliers attribute 1 for raw layer + - name: col_02 + type: int + description: suppliers attribute 2 for raw layer + support.stg_suppliers_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for stg layer + - name: col_01 + type: date + description: suppliers attribute 1 for stg layer + - name: col_02 + type: int + description: suppliers attribute 2 for stg layer + support.int_suppliers_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: suppliers attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for int layer + support.mart_suppliers_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for mart layer + - name: col_01 + type: int + description: suppliers attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: suppliers attribute 2 for mart layer + support.raw_warehouses_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for raw layer + - name: col_01 + type: int + description: warehouses attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for raw layer + support.stg_warehouses_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for stg layer + - name: col_01 + type: int + description: warehouses attribute 1 for stg layer + - name: col_02 + type: int + description: warehouses attribute 2 for stg layer + support.int_warehouses_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for int layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for int layer + - name: col_02 + type: int + description: warehouses attribute 2 for int layer + support.mart_warehouses_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: warehouses attribute 0 for mart layer + - name: col_01 + type: int + description: warehouses attribute 1 for mart layer + - name: col_02 + type: date + description: warehouses attribute 2 for mart layer + support.raw_promotions_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: promotions attribute 0 for raw layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for raw layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for raw layer + support.stg_promotions_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: varchar + description: promotions attribute 2 for stg layer + support.int_promotions_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: int + description: promotions attribute 1 for int layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for int layer + support.mart_promotions_00: + owner_team: support + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: promotions attribute 0 for mart layer + - name: col_01 + type: varchar + description: promotions attribute 1 for mart layer + - name: col_02 + type: int + description: promotions attribute 2 for mart layer + support.raw_attribution_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for raw layer + - name: col_02 + type: int + description: attribution attribute 2 for raw layer + support.stg_attribution_00: + owner_team: support + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: attribution attribute 0 for stg layer + - name: col_01 + type: varchar + description: attribution attribute 1 for stg layer + - name: col_02 + type: date + description: attribution attribute 2 for stg layer + support.int_attribution_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for int layer + - name: col_01 + type: int + description: attribution attribute 1 for int layer + - name: col_02 + type: int + description: attribution attribute 2 for int layer + support.mart_attribution_00: + owner_team: support + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: varchar + description: attribution attribute 2 for mart layer + support.raw_cohorts_00: + owner_team: support + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for raw layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for raw layer + - name: col_02 + type: int + description: cohorts attribute 2 for raw layer + support.stg_cohorts_00: + owner_team: support + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: cohorts attribute 0 for stg layer + - name: col_01 + type: int + description: cohorts attribute 1 for stg layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for stg layer + support.int_cohorts_00: + owner_team: support + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for int layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for int layer + - name: col_02 + type: int + description: cohorts attribute 2 for int layer + support.mart_cohorts_00: + owner_team: support + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: cohorts attribute 0 for mart layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for mart layer + - name: col_02 + type: varchar + description: cohorts attribute 2 for mart layer + people.raw_orders_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for raw layer + - name: col_01 + type: date + description: orders attribute 1 for raw layer + - name: col_02 + type: int + description: orders attribute 2 for raw layer + people.stg_orders_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: orders attribute 0 for stg layer + - name: col_01 + type: varchar + description: orders attribute 1 for stg layer + - name: col_02 + type: varchar + description: orders attribute 2 for stg layer + people.int_orders_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: orders attribute 0 for int layer + - name: col_01 + type: date + description: orders attribute 1 for int layer + - name: col_02 + type: int + description: orders attribute 2 for int layer + people.mart_orders_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: orders attribute 0 for mart layer + - name: col_01 + type: timestamp + description: orders attribute 1 for mart layer + - name: col_02 + type: varchar + description: orders attribute 2 for mart layer + people.raw_invoices_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: invoices attribute 0 for raw layer + - name: col_01 + type: timestamp + description: invoices attribute 1 for raw layer + - name: col_02 + type: varchar + description: invoices attribute 2 for raw layer + people.stg_invoices_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: invoices attribute 0 for stg layer + - name: col_01 + type: varchar + description: invoices attribute 1 for stg layer + - name: col_02 + type: int + description: invoices attribute 2 for stg layer + people.int_invoices_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: invoices attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: invoices attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for int layer + people.mart_invoices_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: invoices attribute 0 for mart layer + - name: col_01 + type: varchar + description: invoices attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: invoices attribute 2 for mart layer + people.raw_ledger_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: ledger attribute 0 for raw layer + - name: col_01 + type: date + description: ledger attribute 1 for raw layer + - name: col_02 + type: varchar + description: ledger attribute 2 for raw layer + people.stg_ledger_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for stg layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for stg layer + - name: col_02 + type: int + description: ledger attribute 2 for stg layer + people.int_ledger_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: ledger attribute 0 for int layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for int layer + - name: col_02 + type: timestamp + description: ledger attribute 2 for int layer + people.mart_ledger_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: ledger attribute 0 for mart layer + - name: col_01 + type: timestamp + description: ledger attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: ledger attribute 2 for mart layer + people.raw_refunds_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: refunds attribute 0 for raw layer + - name: col_01 + type: timestamp + description: refunds attribute 1 for raw layer + - name: col_02 + type: int + description: refunds attribute 2 for raw layer + people.stg_refunds_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: int + description: refunds attribute 0 for stg layer + - name: col_01 + type: varchar + description: refunds attribute 1 for stg layer + - name: col_02 + type: varchar + description: refunds attribute 2 for stg layer + people.int_refunds_00: + owner_team: people + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: refunds attribute 0 for int layer + - name: col_01 + type: varchar + description: refunds attribute 1 for int layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for int layer + people.mart_refunds_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: refunds attribute 0 for mart layer + - name: col_01 + type: int + description: refunds attribute 1 for mart layer + - name: col_02 + type: timestamp + description: refunds attribute 2 for mart layer + people.raw_sessions_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for raw layer + - name: col_01 + type: date + description: sessions attribute 1 for raw layer + - name: col_02 + type: varchar + description: sessions attribute 2 for raw layer + people.stg_sessions_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: sessions attribute 0 for stg layer + - name: col_01 + type: varchar + description: sessions attribute 1 for stg layer + - name: col_02 + type: date + description: sessions attribute 2 for stg layer + people.int_sessions_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: sessions attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: sessions attribute 1 for int layer + - name: col_02 + type: timestamp + description: sessions attribute 2 for int layer + people.mart_sessions_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: sessions attribute 0 for mart layer + - name: col_01 + type: date + description: sessions attribute 1 for mart layer + - name: col_02 + type: date + description: sessions attribute 2 for mart layer + people.raw_signups_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: signups attribute 0 for raw layer + - name: col_01 + type: date + description: signups attribute 1 for raw layer + - name: col_02 + type: date + description: signups attribute 2 for raw layer + people.stg_signups_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: signups attribute 0 for stg layer + - name: col_01 + type: varchar + description: signups attribute 1 for stg layer + - name: col_02 + type: timestamp + description: signups attribute 2 for stg layer + people.int_signups_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: signups attribute 0 for int layer + - name: col_01 + type: timestamp + description: signups attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for int layer + people.mart_signups_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: signups attribute 0 for mart layer + - name: col_01 + type: int + description: signups attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: signups attribute 2 for mart layer + people.raw_campaigns_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: campaigns attribute 0 for raw layer + - name: col_01 + type: varchar + description: campaigns attribute 1 for raw layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for raw layer + people.stg_campaigns_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: campaigns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: campaigns attribute 2 for stg layer + people.int_campaigns_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: campaigns attribute 0 for int layer + - name: col_01 + type: date + description: campaigns attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: campaigns attribute 2 for int layer + people.mart_campaigns_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: campaigns attribute 0 for mart layer + - name: col_01 + type: int + description: campaigns attribute 1 for mart layer + - name: col_02 + type: varchar + description: campaigns attribute 2 for mart layer + people.raw_spend_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for raw layer + - name: col_01 + type: int + description: spend attribute 1 for raw layer + - name: col_02 + type: varchar + description: spend attribute 2 for raw layer + people.stg_spend_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for stg layer + - name: col_01 + type: date + description: spend attribute 1 for stg layer + - name: col_02 + type: date + description: spend attribute 2 for stg layer + people.int_spend_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: spend attribute 0 for int layer + - name: col_01 + type: timestamp + description: spend attribute 1 for int layer + - name: col_02 + type: int + description: spend attribute 2 for int layer + people.mart_spend_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: spend attribute 0 for mart layer + - name: col_01 + type: timestamp + description: spend attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: spend attribute 2 for mart layer + people.raw_fraud_scores_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: fraud_scores attribute 0 for raw layer + - name: col_01 + type: int + description: fraud_scores attribute 1 for raw layer + - name: col_02 + type: timestamp + description: fraud_scores attribute 2 for raw layer + people.stg_fraud_scores_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for stg layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for stg layer + - name: col_02 + type: int + description: fraud_scores attribute 2 for stg layer + people.int_fraud_scores_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: fraud_scores attribute 0 for int layer + - name: col_01 + type: varchar + description: fraud_scores attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: fraud_scores attribute 2 for int layer + people.mart_fraud_scores_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: fraud_scores attribute 0 for mart layer + - name: col_01 + type: timestamp + description: fraud_scores attribute 1 for mart layer + - name: col_02 + type: date + description: fraud_scores attribute 2 for mart layer + people.raw_chargebacks_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for raw layer + - name: col_01 + type: timestamp + description: chargebacks attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for raw layer + people.stg_chargebacks_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: chargebacks attribute 0 for stg layer + - name: col_01 + type: int + description: chargebacks attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: chargebacks attribute 2 for stg layer + people.int_chargebacks_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: chargebacks attribute 0 for int layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for int layer + - name: col_02 + type: date + description: chargebacks attribute 2 for int layer + people.mart_chargebacks_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: chargebacks attribute 0 for mart layer + - name: col_01 + type: varchar + description: chargebacks attribute 1 for mart layer + - name: col_02 + type: date + description: chargebacks attribute 2 for mart layer + people.raw_inventory_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: date + description: inventory attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: inventory attribute 1 for raw layer + - name: col_02 + type: int + description: inventory attribute 2 for raw layer + people.stg_inventory_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for stg layer + - name: col_01 + type: int + description: inventory attribute 1 for stg layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for stg layer + people.int_inventory_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: inventory attribute 0 for int layer + - name: col_01 + type: date + description: inventory attribute 1 for int layer + - name: col_02 + type: date + description: inventory attribute 2 for int layer + people.mart_inventory_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: inventory attribute 0 for mart layer + - name: col_01 + type: varchar + description: inventory attribute 1 for mart layer + - name: col_02 + type: timestamp + description: inventory attribute 2 for mart layer + people.raw_shipments_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: shipments attribute 0 for raw layer + - name: col_01 + type: date + description: shipments attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: shipments attribute 2 for raw layer + people.stg_shipments_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for stg layer + - name: col_01 + type: varchar + description: shipments attribute 1 for stg layer + - name: col_02 + type: date + description: shipments attribute 2 for stg layer + people.int_shipments_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: shipments attribute 0 for int layer + - name: col_01 + type: varchar + description: shipments attribute 1 for int layer + - name: col_02 + type: int + description: shipments attribute 2 for int layer + people.mart_shipments_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: shipments attribute 0 for mart layer + - name: col_01 + type: timestamp + description: shipments attribute 1 for mart layer + - name: col_02 + type: varchar + description: shipments attribute 2 for mart layer + people.raw_carriers_00: + owner_team: people + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: carriers attribute 0 for raw layer + - name: col_01 + type: varchar + description: carriers attribute 1 for raw layer + - name: col_02 + type: date + description: carriers attribute 2 for raw layer + people.stg_carriers_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for stg layer + - name: col_01 + type: date + description: carriers attribute 1 for stg layer + - name: col_02 + type: varchar + description: carriers attribute 2 for stg layer + people.int_carriers_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: carriers attribute 0 for int layer + - name: col_01 + type: int + description: carriers attribute 1 for int layer + - name: col_02 + type: int + description: carriers attribute 2 for int layer + people.mart_carriers_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: carriers attribute 0 for mart layer + - name: col_01 + type: varchar + description: carriers attribute 1 for mart layer + - name: col_02 + type: varchar + description: carriers attribute 2 for mart layer + people.raw_products_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: products attribute 0 for raw layer + - name: col_01 + type: timestamp + description: products attribute 1 for raw layer + - name: col_02 + type: int + description: products attribute 2 for raw layer + people.stg_products_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: products attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: products attribute 1 for stg layer + - name: col_02 + type: date + description: products attribute 2 for stg layer + people.int_products_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: products attribute 0 for int layer + - name: col_01 + type: varchar + description: products attribute 1 for int layer + - name: col_02 + type: timestamp + description: products attribute 2 for int layer + people.mart_products_00: + owner_team: people + retention_days: 730 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: products attribute 0 for mart layer + - name: col_01 + type: timestamp + description: products attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: products attribute 2 for mart layer + people.raw_prices_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for raw layer + - name: col_01 + type: varchar + description: prices attribute 1 for raw layer + - name: col_02 + type: date + description: prices attribute 2 for raw layer + people.stg_prices_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: prices attribute 0 for stg layer + - name: col_01 + type: timestamp + description: prices attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for stg layer + people.int_prices_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: prices attribute 0 for int layer + - name: col_01 + type: varchar + description: prices attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: prices attribute 2 for int layer + people.mart_prices_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: prices attribute 0 for mart layer + - name: col_01 + type: varchar + description: prices attribute 1 for mart layer + - name: col_02 + type: int + description: prices attribute 2 for mart layer + people.raw_queries_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: queries attribute 0 for raw layer + - name: col_01 + type: int + description: queries attribute 1 for raw layer + - name: col_02 + type: varchar + description: queries attribute 2 for raw layer + people.stg_queries_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: queries attribute 0 for stg layer + - name: col_01 + type: timestamp + description: queries attribute 1 for stg layer + - name: col_02 + type: varchar + description: queries attribute 2 for stg layer + people.int_queries_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: int + description: queries attribute 0 for int layer + - name: col_01 + type: varchar + description: queries attribute 1 for int layer + - name: col_02 + type: date + description: queries attribute 2 for int layer + people.mart_queries_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: queries attribute 0 for mart layer + - name: col_01 + type: timestamp + description: queries attribute 1 for mart layer + - name: col_02 + type: date + description: queries attribute 2 for mart layer + people.raw_clicks_00: + owner_team: people + retention_days: 730 + pii: true + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for raw layer + - name: col_01 + type: int + description: clicks attribute 1 for raw layer + - name: col_02 + type: timestamp + description: clicks attribute 2 for raw layer + people.stg_clicks_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: clicks attribute 1 for stg layer + - name: col_02 + type: varchar + description: clicks attribute 2 for stg layer + people.int_clicks_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: clicks attribute 0 for int layer + - name: col_01 + type: int + description: clicks attribute 1 for int layer + - name: col_02 + type: date + description: clicks attribute 2 for int layer + people.mart_clicks_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: clicks attribute 0 for mart layer + - name: col_01 + type: int + description: clicks attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: clicks attribute 2 for mart layer + people.raw_payouts_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for raw layer + - name: col_01 + type: date + description: payouts attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for raw layer + people.stg_payouts_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: payouts attribute 0 for stg layer + - name: col_01 + type: varchar + description: payouts attribute 1 for stg layer + - name: col_02 + type: timestamp + description: payouts attribute 2 for stg layer + people.int_payouts_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: payouts attribute 0 for int layer + - name: col_01 + type: timestamp + description: payouts attribute 1 for int layer + - name: col_02 + type: int + description: payouts attribute 2 for int layer + people.mart_payouts_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payouts attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: payouts attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: payouts attribute 2 for mart layer + people.raw_settlements_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for raw layer + - name: col_01 + type: int + description: settlements attribute 1 for raw layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for raw layer + people.stg_settlements_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: settlements attribute 0 for stg layer + - name: col_01 + type: varchar + description: settlements attribute 1 for stg layer + - name: col_02 + type: date + description: settlements attribute 2 for stg layer + people.int_settlements_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: int + description: settlements attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: settlements attribute 1 for int layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for int layer + people.mart_settlements_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: settlements attribute 0 for mart layer + - name: col_01 + type: timestamp + description: settlements attribute 1 for mart layer + - name: col_02 + type: timestamp + description: settlements attribute 2 for mart layer + people.raw_tickets_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: timestamp + description: tickets attribute 0 for raw layer + - name: col_01 + type: date + description: tickets attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: tickets attribute 2 for raw layer + people.stg_tickets_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: tickets attribute 0 for stg layer + - name: col_01 + type: date + description: tickets attribute 1 for stg layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for stg layer + people.int_tickets_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: tickets attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: tickets attribute 1 for int layer + - name: col_02 + type: varchar + description: tickets attribute 2 for int layer + people.mart_tickets_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: decimal(18,2) + description: tickets attribute 0 for mart layer + - name: col_01 + type: int + description: tickets attribute 1 for mart layer + - name: col_02 + type: timestamp + description: tickets attribute 2 for mart layer + people.raw_csat_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: date + description: csat attribute 0 for raw layer + - name: col_01 + type: date + description: csat attribute 1 for raw layer + - name: col_02 + type: varchar + description: csat attribute 2 for raw layer + people.stg_csat_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for stg layer + - name: col_01 + type: timestamp + description: csat attribute 1 for stg layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for stg layer + people.int_csat_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: csat attribute 0 for int layer + - name: col_01 + type: date + description: csat attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: csat attribute 2 for int layer + people.mart_csat_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: timestamp + description: csat attribute 0 for mart layer + - name: col_01 + type: int + description: csat attribute 1 for mart layer + - name: col_02 + type: varchar + description: csat attribute 2 for mart layer + people.raw_headcount_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: headcount attribute 0 for raw layer + - name: col_01 + type: int + description: headcount attribute 1 for raw layer + - name: col_02 + type: varchar + description: headcount attribute 2 for raw layer + people.stg_headcount_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for stg layer + - name: col_01 + type: timestamp + description: headcount attribute 1 for stg layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for stg layer + people.int_headcount_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: headcount attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for int layer + - name: col_02 + type: timestamp + description: headcount attribute 2 for int layer + people.mart_headcount_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: headcount attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: headcount attribute 1 for mart layer + - name: col_02 + type: int + description: headcount attribute 2 for mart layer + people.raw_payroll_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: payroll attribute 0 for raw layer + - name: col_01 + type: varchar + description: payroll attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for raw layer + people.stg_payroll_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for stg layer + - name: col_01 + type: date + description: payroll attribute 1 for stg layer + - name: col_02 + type: varchar + description: payroll attribute 2 for stg layer + people.int_payroll_00: + owner_team: people + retention_days: 730 + pii: true + columns: + - name: col_00 + type: date + description: payroll attribute 0 for int layer + - name: col_01 + type: varchar + description: payroll attribute 1 for int layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for int layer + people.mart_payroll_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: payroll attribute 0 for mart layer + - name: col_01 + type: date + description: payroll attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: payroll attribute 2 for mart layer + people.raw_forecast_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for raw layer + - name: col_01 + type: date + description: forecast attribute 1 for raw layer + - name: col_02 + type: date + description: forecast attribute 2 for raw layer + people.stg_forecast_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: forecast attribute 0 for stg layer + - name: col_01 + type: int + description: forecast attribute 1 for stg layer + - name: col_02 + type: varchar + description: forecast attribute 2 for stg layer + people.int_forecast_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: forecast attribute 0 for int layer + - name: col_01 + type: varchar + description: forecast attribute 1 for int layer + - name: col_02 + type: int + description: forecast attribute 2 for int layer + people.mart_forecast_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: timestamp + description: forecast attribute 0 for mart layer + - name: col_01 + type: decimal(18,2) + description: forecast attribute 1 for mart layer + - name: col_02 + type: varchar + description: forecast attribute 2 for mart layer + people.raw_returns_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: returns attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: returns attribute 2 for raw layer + people.stg_returns_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for stg layer + - name: col_01 + type: timestamp + description: returns attribute 1 for stg layer + - name: col_02 + type: timestamp + description: returns attribute 2 for stg layer + people.int_returns_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: returns attribute 0 for int layer + - name: col_01 + type: int + description: returns attribute 1 for int layer + - name: col_02 + type: date + description: returns attribute 2 for int layer + people.mart_returns_00: + owner_team: people + retention_days: 365 + pii: true + columns: + - name: col_00 + type: varchar + description: returns attribute 0 for mart layer + - name: col_01 + type: varchar + description: returns attribute 1 for mart layer + - name: col_02 + type: timestamp + description: returns attribute 2 for mart layer + people.raw_suppliers_00: + owner_team: people + retention_days: 90 + pii: true + columns: + - name: col_00 + type: timestamp + description: suppliers attribute 0 for raw layer + - name: col_01 + type: date + description: suppliers attribute 1 for raw layer + - name: col_02 + type: timestamp + description: suppliers attribute 2 for raw layer + people.stg_suppliers_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: suppliers attribute 1 for stg layer + - name: col_02 + type: varchar + description: suppliers attribute 2 for stg layer + people.int_suppliers_00: + owner_team: people + retention_days: 30 + pii: true + columns: + - name: col_00 + type: date + description: suppliers attribute 0 for int layer + - name: col_01 + type: timestamp + description: suppliers attribute 1 for int layer + - name: col_02 + type: date + description: suppliers attribute 2 for int layer + people.mart_suppliers_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: suppliers attribute 0 for mart layer + - name: col_01 + type: date + description: suppliers attribute 1 for mart layer + - name: col_02 + type: date + description: suppliers attribute 2 for mart layer + people.raw_warehouses_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for raw layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for raw layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for raw layer + people.stg_warehouses_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: varchar + description: warehouses attribute 0 for stg layer + - name: col_01 + type: varchar + description: warehouses attribute 1 for stg layer + - name: col_02 + type: date + description: warehouses attribute 2 for stg layer + people.int_warehouses_00: + owner_team: people + retention_days: 730 + pii: false + columns: + - name: col_00 + type: date + description: warehouses attribute 0 for int layer + - name: col_01 + type: decimal(18,2) + description: warehouses attribute 1 for int layer + - name: col_02 + type: timestamp + description: warehouses attribute 2 for int layer + people.mart_warehouses_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: warehouses attribute 0 for mart layer + - name: col_01 + type: timestamp + description: warehouses attribute 1 for mart layer + - name: col_02 + type: decimal(18,2) + description: warehouses attribute 2 for mart layer + people.raw_promotions_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for raw layer + - name: col_01 + type: date + description: promotions attribute 1 for raw layer + - name: col_02 + type: int + description: promotions attribute 2 for raw layer + people.stg_promotions_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for stg layer + - name: col_01 + type: date + description: promotions attribute 1 for stg layer + - name: col_02 + type: int + description: promotions attribute 2 for stg layer + people.int_promotions_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: varchar + description: promotions attribute 0 for int layer + - name: col_01 + type: int + description: promotions attribute 1 for int layer + - name: col_02 + type: timestamp + description: promotions attribute 2 for int layer + people.mart_promotions_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: int + description: promotions attribute 0 for mart layer + - name: col_01 + type: timestamp + description: promotions attribute 1 for mart layer + - name: col_02 + type: date + description: promotions attribute 2 for mart layer + people.raw_attribution_00: + owner_team: people + retention_days: 365 + pii: false + columns: + - name: col_00 + type: int + description: attribution attribute 0 for raw layer + - name: col_01 + type: int + description: attribution attribute 1 for raw layer + - name: col_02 + type: varchar + description: attribution attribute 2 for raw layer + people.stg_attribution_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for stg layer + - name: col_01 + type: decimal(18,2) + description: attribution attribute 1 for stg layer + - name: col_02 + type: int + description: attribution attribute 2 for stg layer + people.int_attribution_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: decimal(18,2) + description: attribution attribute 0 for int layer + - name: col_01 + type: varchar + description: attribution attribute 1 for int layer + - name: col_02 + type: date + description: attribution attribute 2 for int layer + people.mart_attribution_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: date + description: attribution attribute 0 for mart layer + - name: col_01 + type: int + description: attribution attribute 1 for mart layer + - name: col_02 + type: int + description: attribution attribute 2 for mart layer + people.raw_cohorts_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: date + description: cohorts attribute 0 for raw layer + - name: col_01 + type: timestamp + description: cohorts attribute 1 for raw layer + - name: col_02 + type: date + description: cohorts attribute 2 for raw layer + people.stg_cohorts_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for stg layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for stg layer + - name: col_02 + type: date + description: cohorts attribute 2 for stg layer + people.int_cohorts_00: + owner_team: people + retention_days: 30 + pii: false + columns: + - name: col_00 + type: timestamp + description: cohorts attribute 0 for int layer + - name: col_01 + type: date + description: cohorts attribute 1 for int layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for int layer + people.mart_cohorts_00: + owner_team: people + retention_days: 90 + pii: false + columns: + - name: col_00 + type: int + description: cohorts attribute 0 for mart layer + - name: col_01 + type: varchar + description: cohorts attribute 1 for mart layer + - name: col_02 + type: timestamp + description: cohorts attribute 2 for mart layer diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/dags/pipeline_factory.py new file mode 100644 index 0000000..96daadf --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/dags/pipeline_factory.py @@ -0,0 +1,96 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. +""" + +from __future__ import annotations + +import json +from datetime import datetime, timedelta +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=yaml.SafeLoader) + + +def check_table(registry: dict, table: str) -> None: + if table not in registry["datasets"]: + raise ValueError(f"unknown table {table!r}: register it in the platform catalog first") + + +def start_minute(name: str) -> int: + # Spread pipelines over the hour so they do not all hit the warehouse at :00. + return hash(name) % 60 + + +def build_dag(path: Path) -> DAG: + cfg = load_yaml(path) + registry = load_yaml(REGISTRY) + team = registry["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = set(team["owners"]) | set(cfg.get("owners", [])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + doc_md=f"Generated from `{path.name}` on {datetime.now():%Y-%m-%d %H:%M}. " + f"On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + check_table(registry, step["table"]) + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + return dag + + +for config_path in CONFIG_DIR.glob("*.yaml"): + dag = build_dag(config_path) + globals()[dag.dag_id] = dag diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/pools.json b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/pools.json new file mode 100644 index 0000000..bc85e99 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/pools.json @@ -0,0 +1,3 @@ +{ + "default_pool": {"slots": 128, "description": "Default pool", "include_deferred": false} +} diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/fixture/requirements.txt b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/requirements.txt new file mode 100644 index 0000000..47e6eb1 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +PyYAML==6.0.3 diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/grade.py b/evals/airflow/cases/hard-reliability-factory-fleet/grade.py new file mode 100644 index 0000000..afe1cf4 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/grade.py @@ -0,0 +1,495 @@ +"""Grader for hard-reliability-factory-fleet. + +The fixture factory builds 140 DAGs from configs/pipelines/*.yaml. It re-reads +the 0.5 MB platform registry with the pure-Python YAML loader for every +pipeline (the file takes over a minute to parse, so the dag-processor kills it +at the 30 s import timeout), lets one bad config raise out of the module (every +DAG disappears), and puts per-process values into the DAGs: `hash(name) % 60` +as the start minute (salted per process), a `set` joined into `owner`, and the +parse time in `doc_md`. Workers parse all 140 DAGs to run one task. + +The grader parses the fixed repo the way the dag-processor and workers do and +checks each requirement from the prompt by behaviour: +- every pipeline config still yields its DAG with the same tasks, edges, + owners, retries and run times (cron fire times compared with a reference + timetable built from the config); +- start minutes stay spread over the hour; explicit `minute:` is honoured; +- the file parses in <= PARSE_BUDGET_S (median of three parses); +- three fresh parses (different PYTHONHASHSEED, TZ and a clock moved ahead) + give every DAG the same Airflow `dag_hash`; +- broken configs (syntax error, empty file, unknown team, unknown dependency, + invalid day-of-week, unregistered table) only remove their own DAG, are named + in the parse output, and leave every other DAG's hash unchanged; adding and + removing a config does not change any other DAG either; +- with the worker parsing context set to one dag_id, only that DAG is built, + and it is the same DAG as in a full parse; +- `airflow dags test` of two pipelines runs every step once, in dependency order. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +from pathlib import Path + +import yaml + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +PARSE_BUDGET_S = 5.0 +CLOCK_SHIFT_S = str(3 * 86400 + 7 * 3600) +PARSE_SCENARIOS = [("1", "UTC", "0"), ("2", "Pacific/Kiritimati", "0"), ("3", "Etc/GMT+12", CLOCK_SHIFT_S)] +# Pipelines executed with `airflow dags test` (one linear, one with a skip edge). +RUN_PIPELINES = ("billing_carriers", "billing_promotions") +# Pipelines parsed under a worker parsing context. +CONTEXT_PIPELINES = ("finance_payouts", "support_payroll") +MIN_DISTINCT_MINUTES = 30 +WAREHOUSE_POOL = "warehouse" +WAREHOUSE_SLOTS = 16 +BIG_STEPS = 400 +MAX_PER_MINUTE = 8 + +BROKEN = { + "aa_unknown_team.yaml": {"name": "aa_unknown_team", "team": "astronomy", + "schedule": {"hour": 2}, "steps": [{"name": "extract", "table": "billing.raw_orders_00"}]}, + "mm_bad_dependency.yaml": {"name": "mm_bad_dependency", "team": "billing", "schedule": {"hour": 2}, + "steps": [{"name": "extract", "table": "billing.raw_orders_00"}, + {"name": "publish", "table": "billing.mart_orders_00", + "depends_on": ["transform"]}]}, + "mm_bad_days.yaml": {"name": "mm_bad_days", "team": "risk", "schedule": {"hour": 2, "days": "funday"}, + "steps": [{"name": "extract", "table": "risk.raw_orders_00"}]}, + "mm_cycle.yaml": {"name": "mm_cycle", "team": "supply", "schedule": {"hour": 3}, + "steps": [{"name": "extract", "table": "supply.raw_orders_00", "depends_on": ["publish"]}, + {"name": "publish", "table": "supply.mart_orders_00", "depends_on": ["extract"]}]}, + "mm_duplicate_step.yaml": {"name": "mm_duplicate_step", "team": "people", "schedule": {"hour": 3}, + "steps": [{"name": "extract", "table": "people.raw_orders_00"}, + {"name": "extract", "table": "people.raw_orders_00"}, + {"name": "publish", "table": "people.mart_orders_00", + "depends_on": ["extract"]}]}, + "mm_unregistered_table.yaml": {"name": "mm_unregistered_table", "team": "search", "schedule": {"hour": 2}, + "steps": [{"name": "extract", "table": "search.raw_does_not_exist_00"}]}, +} +BROKEN_TEXT = { + "zz_broken_syntax.yaml": "name: zz_broken_syntax\nteam: billing\nschedule:\n hour: 2\n steps:\n - name: extract\n", + "nn_empty.yaml": "", +} +NEW_CONFIG = {"name": "zz_new_pipeline", "team": "growth", "schedule": {"hour": 5}, + "steps": [{"name": "extract", "table": "growth.raw_orders_00"}, + {"name": "publish", "table": "growth.mart_orders_00", "depends_on": ["extract"]}]} +# Grader-written configs carry every field the fleet's configs have (all 140 set +# `tier` and `description`), so a factory that requires them is not penalised and +# each broken config has exactly the one defect it is named after. +for _cfg in [*BROKEN.values(), NEW_CONFIG]: + _cfg.setdefault("tier", "standard") + _cfg.setdefault("description", f"{_cfg['name']} pipeline") + +PARSE_PROBE = r""" +import json, os, time +import datetime as _dtmod +import pendulum, yaml # noqa: F401 +import airflow.sdk # noqa: F401 +import airflow.providers.standard.operators.python # noqa: F401 +from airflow.dag_processing.dagbag import DagBag # noqa: F401 +from airflow.models.serialized_dag import SerializedDagModel +from airflow.serialization.serialized_objects import DagSerialization +from airflow.timetables.trigger import CronTriggerTimetable # noqa: F401 + +_shift = _dtmod.timedelta(seconds=float(os.environ.get("EVAL_CLOCK_SHIFT_S", "0"))) +_real_dt, _real_date = _dtmod.datetime, _dtmod.date +if _shift: + class _Meta(type): + def __instancecheck__(cls, obj): + return isinstance(obj, cls._real) + + def __subclasscheck__(cls, sub): + return issubclass(sub, cls._real) + + class _ShiftedDatetime(_real_dt, metaclass=_Meta): + _real = _real_dt + + def __new__(cls, *args, **kwargs): + return _real_dt(*args, **kwargs) + + @classmethod + def now(cls, tz=None): + return _real_dt.now(tz) + _shift + + @classmethod + def utcnow(cls): + return _real_dt.utcnow() + _shift + + @classmethod + def today(cls): + return _real_dt.today() + _shift + + class _ShiftedDate(_real_date, metaclass=_Meta): + _real = _real_date + + def __new__(cls, *args, **kwargs): + return _real_date(*args, **kwargs) + + @classmethod + def today(cls): + return (_real_dt.now() + _shift).date() + + _dtmod.datetime, _dtmod.date = _ShiftedDatetime, _ShiftedDate +try: + bag = dagbag() +finally: + _dtmod.datetime, _dtmod.date = _real_dt, _real_date + +def task_weights(dag): + # The priority the scheduler stores on each task instance: the serialized + # (scheduler-side) task's weight rule applied to a task instance. + import uuid + from airflow.models.taskinstance import TaskInstance + sdag = DagSerialization.from_dict(DagSerialization.to_dict(dag)) + ws = [t.weight_rule.get_weight(TaskInstance(task=t, run_id="probe", map_index=-1, dag_version_id=uuid.uuid4())) + for t in sdag.tasks] + return [min(ws), max(ws)] + + +WANT_DETAILS = set(json.loads(os.environ.get("EVAL_DETAIL_DAGS", "[]"))) +out = {} +for dag_id, dag in bag.dags.items(): + entry = {"hash": SerializedDagModel.hash(DagSerialization.to_dict(dag))} + if WANT_DETAILS == {"*"} or dag_id in WANT_DETAILS: + runs = [] + try: + runs = [r["run_after"] for r in scheduled_intervals(dag, "2030-01-06T00:00:00+00:00", n=8)] + except Exception as exc: + runs = [f"error: {exc!r}"] + entry.update({ + "tasks": sorted(t.task_id for t in dag.tasks), + "deps": sorted([t.task_id, d] for t in dag.tasks for d in t.downstream_task_ids), + "owners": sorted(o.strip() for t in dag.tasks[:1] for o in str(t.owner).split(",")), + "retries": sorted({t.retries for t in dag.tasks}), + "pools": sorted({str(t.pool) for t in dag.tasks}), + "prio": task_weights(dag), + "runs": runs, + }) + out[dag_id] = entry +folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] +RESULT = {"dags": out, + "import_errors": {os.path.basename(k): v[-600:] for k, v in bag.import_errors.items()}, + "warnings": [str(w)[:300] for ws in (getattr(bag, "captured_warnings", None) or {}).values() for w in ws], + "parse_s": max([s.duration.total_seconds() for s in bag.dagbag_stats] or [None])} +""" + +EXPECTED_RUNS_PROBE = r""" +import json, os +from airflow.timetables.trigger import CronTriggerTimetable +specs = json.loads(os.environ["EVAL_SPECS"]) + +class _D: # minimal stand-in so scheduled_intervals() can walk a bare timetable + def __init__(self, tt): + self.timetable = tt + self.start_date = None + self.end_date = None + self.catchup = True + +RESULT = {} +for name, expr in specs.items(): + RESULT[name] = [r["run_after"] for r in scheduled_intervals(_D(CronTriggerTimetable(expr, timezone="UTC")), + "2030-01-06T00:00:00+00:00", n=8)] +""" + + +def pristine_configs() -> dict[str, dict]: + out = {} + for path in sorted((CASE_DIR / "fixture" / "configs" / "pipelines").glob("*.yaml")): + cfg = yaml.safe_load(path.read_text()) + out[cfg["name"]] = cfg + return out + + +def registry() -> dict: + return yaml.load((CASE_DIR / "fixture" / "configs" / "registry.yaml").read_text(), Loader=yaml.CSafeLoader) + + +def expected_shape(cfg: dict, reg: dict) -> dict: + team = reg["teams"][cfg["team"]] + return { + "tasks": sorted(s["name"] for s in cfg["steps"]), + "deps": sorted([u, s["name"]] for s in cfg["steps"] for u in s.get("depends_on", [])), + "owners": sorted(set(team["owners"]) | set(cfg.get("owners", []))), + "retries": [cfg.get("retries", team["default_retries"])], + } + + +def parse(py: str, ws: Path, details=(), extra: dict | None = None, seed="1", tz="UTC", shift="0", timeout=400): + env = g.airflow_env(ws, extra={"PYTHONHASHSEED": seed, "TZ": tz, "EVAL_CLOCK_SHIFT_S": shift, + "EVAL_DETAIL_DAGS": json.dumps(list(details)), **(extra or {})}) + res, proc = g.probe_json(py, PARSE_PROBE, ws, env=env, timeout=timeout) + if not isinstance(res, dict): + return {"error": proc.tail(25)}, proc.output + return res, proc.output + + +def check_shapes(py: str, ws: Path, full: dict, configs: dict, reg: dict, grader: g.Grader) -> None: + dags = full["dags"] + bad = [] + for name, cfg in configs.items(): + got = dags.get(name) + if got is None: + continue + want = expected_shape(cfg, reg) + diffs = {k: (got.get(k), v) for k, v in want.items() if got.get(k) != v} + if diffs: + bad.append(f"{name}: {diffs}") + grader.primary("each DAG keeps its config's tasks, dependencies, owners and retries", not bad, + "; ".join(bad[:5]) + (f" (+{len(bad) - 5} more)" if len(bad) > 5 else "")) + + # Fire times: compare against a reference timetable built from the config. + explicit = {n: c["schedule"]["minute"] for n, c in configs.items() if "minute" in c["schedule"]} + specs, minutes, problems = {}, {}, [] + for name, cfg in configs.items(): + runs = dags.get(name, {}).get("runs") or [] + if not runs or any(str(r).startswith("error") for r in runs): + problems.append(f"{name}: no schedule ({runs[:1]})") + continue + minute = int(runs[0][14:16]) + minutes[name] = minute + specs[name] = f"{explicit.get(name, minute)} {cfg['schedule']['hour']} * * {cfg['schedule'].get('days', '*')}" + env = g.airflow_env(ws, extra={"EVAL_SPECS": json.dumps(specs)}) + expected, proc = g.probe_json(py, EXPECTED_RUNS_PROBE, ws, env=env, timeout=300) + if expected is None: + grader.primary("each DAG runs at its configured hour/days (and explicit minute)", False, proc.tail(10)) + else: + for name in specs: + if dags[name]["runs"] != expected[name]: + problems.append(f"{name}: runs {dags[name]['runs'][:3]} expected {expected[name][:3]} " + f"(spec {specs[name]})") + grader.primary("each DAG runs at its configured hour/days (and explicit minute)", not problems, + "; ".join(problems[:5]) + (f" (+{len(problems) - 5} more)" if len(problems) > 5 else "")) + spread = [m for n, m in minutes.items() if n not in explicit] + counts = {m: spread.count(m) for m in set(spread)} + ok = len(counts) >= MIN_DISTINCT_MINUTES and max(counts.values() or [0]) <= MAX_PER_MINUTE + grader.primary("start minutes stay spread over the hour", ok, + f"{len(spread)} pipelines without an explicit minute use {len(counts)} distinct minutes " + f"(need >= {MIN_DISTINCT_MINUTES}); busiest minute has {max(counts.values() or [0])} " + f"(max {MAX_PER_MINUTE})") + + +def check_isolation(py: str, ws: Path, configs: dict, grader: g.Grader) -> None: + # Work on a copy; DAG hashes include the file location, so the baseline is + # parsed from the same copy before any config is touched. + copy = g.copy_workspace(ws) + try: + pdir = copy / "configs" / "pipelines" + if not pdir.is_dir(): + grader.primary("broken configs only take out their own DAG", False, "configs/pipelines is missing") + return + base, _ = parse(py, copy) + if "error" in base: + grader.primary("broken configs only take out their own DAG", False, f"probe failed: {base['error']}") + return + base_hashes = {n: d["hash"] for n, d in base["dags"].items()} + for fname, cfg in BROKEN.items(): + (pdir / fname).write_text(yaml.safe_dump(cfg, sort_keys=False)) + for fname, text in BROKEN_TEXT.items(): + (pdir / fname).write_text(text) + res, output = parse(py, copy) + if "error" in res: + grader.primary("broken configs only take out their own DAG", False, f"probe failed: {res['error']}") + return + dags = res["dags"] + missing = sorted(set(configs) - set(dags)) + changed = sorted(n for n in configs if n in dags and dags[n]["hash"] != base_hashes.get(n)) + leaked = sorted(set(dags) & {c["name"] for c in BROKEN.values()}) + ok = not missing and not changed and not leaked + grader.primary("broken configs only take out their own DAG (others load unchanged, no partial DAGs)", ok, + f"missing {len(missing)} DAGs {missing[:5]}; changed hashes {changed[:5]}; " + f"half-built broken DAGs loaded {leaked}; import errors " + f"{ {k: v.strip().splitlines()[-1][:160] for k, v in res['import_errors'].items()} }") + haystack = output + "\n".join(res.get("warnings", [])) + unnamed = sorted(f for f in [*BROKEN, *BROKEN_TEXT] if f not in haystack) + grader.primary("each broken config is reported by file name in the parse output", not unnamed, + f"not mentioned: {unnamed}") + + # Adding a pipeline and removing another leaves every other DAG alone. + for fname in [*BROKEN, *BROKEN_TEXT]: + (pdir / fname).unlink() + removed = sorted(configs)[0] + for path in pdir.glob("*.yaml"): + if yaml.safe_load(path.read_text()).get("name") == removed: + path.unlink() + (pdir / "zz_new_pipeline.yaml").write_text(yaml.safe_dump(NEW_CONFIG, sort_keys=False)) + res, _ = parse(py, copy) + if "error" in res: + grader.primary("adding/removing a config leaves every other DAG unchanged", False, res["error"]) + return + dags = res["dags"] + changed = sorted(n for n in configs if n != removed and dags.get(n, {}).get("hash") != base_hashes.get(n)) + ok = not changed and "zz_new_pipeline" in dags and removed not in dags + grader.primary("adding/removing a config leaves every other DAG unchanged", ok, + f"changed or missing: {changed[:6]} ({len(changed)}); new present=" + f"{'zz_new_pipeline' in dags}; removed {removed} gone={removed not in dags}") + finally: + shutil.rmtree(copy.parent, ignore_errors=True) + + +def big_standard_config() -> dict: + steps = [{"name": "extract", "table": "growth.raw_orders_00"}] + for i in range(1, BIG_STEPS): + steps.append({"name": f"step_{i:03d}", "table": "growth.int_orders_00", "depends_on": [steps[-1]["name"]]}) + return {"name": "zz_big_standard", "team": "growth", "tier": "standard", "description": "Big standard pipeline", + "schedule": {"hour": 1}, "steps": steps} + + +TINY_CRITICAL = {"name": "zz_tiny_critical", "team": "finance", "tier": "critical", + "description": "Tiny critical pipeline", "schedule": {"hour": 1}, + "steps": [{"name": "extract", "table": "finance.raw_orders_00"}]} + + +def check_concurrency(py: str, ws: Path, configs: dict, grader: g.Grader) -> None: + # Pool definition: the deploy runs `airflow pools import pools.json`. + env = g.airflow_env(ws) + db = g.ensure_db(py, env) + pools_file = ws / "pools.json" + slots, detail = None, "" + if not pools_file.is_file(): + detail = "pools.json is missing" + elif not db.ok: + detail = db.tail(10) + else: + imp = g.run_cmd([str(Path(py).parent / "airflow"), "pools", "import", str(pools_file)], env=env, cwd=ws) + res, proc = g.probe_json(py, """ + from airflow.models.pool import Pool + from airflow.utils.session import create_session + with create_session() as s: + RESULT = {p.pool: p.slots for p in s.query(Pool).all()} + """, ws, env=env) + slots = (res or {}).get(WAREHOUSE_POOL) + detail = f"pools after import: {res}" + ("" if imp.ok else f"; import failed: {imp.tail(5)}") + grader.primary(f"pools.json defines pool {WAREHOUSE_POOL!r} with {WAREHOUSE_SLOTS} slots", + slots == WAREHOUSE_SLOTS, detail) + + # Every fleet task runs in the warehouse pool, and critical outranks standard + # even with a very long standard pipeline and a one-step critical one added. + copy = g.copy_workspace(ws) + try: + pdir = copy / "configs" / "pipelines" + pdir.mkdir(parents=True, exist_ok=True) + (pdir / "zz_big_standard.yaml").write_text(yaml.safe_dump(big_standard_config(), sort_keys=False)) + (pdir / "zz_tiny_critical.yaml").write_text(yaml.safe_dump(TINY_CRITICAL, sort_keys=False)) + res, _ = parse(py, copy, details=("*",)) + if "error" in res: + grader.primary("every fleet task runs in the warehouse pool", False, res["error"]) + grader.primary("critical tasks always outrank standard tasks", False, res["error"]) + return + dags = res["dags"] + tiers = {n: c.get("tier", "standard") for n, c in configs.items()} + tiers.update({"zz_big_standard": "standard", "zz_tiny_critical": "critical"}) + absent = sorted(n for n in tiers if n not in dags) + wrong_pool = sorted(n for n in tiers if n in dags and dags[n].get("pools") != [WAREHOUSE_POOL]) + grader.primary("every fleet task runs in the warehouse pool", not wrong_pool and not absent, + f"DAGs with other pools: {[(n, dags[n].get('pools')) for n in wrong_pool[:4]]}; " + f"missing DAGs: {absent[:4]}") + crit = [dags[n]["prio"][0] for n, t in tiers.items() if t == "critical" and n in dags] + std = [dags[n]["prio"][1] for n, t in tiers.items() if t == "standard" and n in dags] + ok = bool(crit) and bool(std) and not absent and min(crit) > max(std) + lowest = min(((dags[n]["prio"][0], n) for n, t in tiers.items() if t == "critical" and n in dags), default=None) + highest = max(((dags[n]["prio"][1], n) for n, t in tiers.items() if t == "standard" and n in dags), default=None) + grader.primary("critical tasks always outrank standard tasks (scheduler priority weight)", ok, + f"lowest critical task weight {lowest}; highest standard task weight {highest} " + f"(with a {BIG_STEPS}-step standard pipeline and a 1-step critical pipeline added)") + finally: + shutil.rmtree(copy.parent, ignore_errors=True) + + +def check_parsing_context(py: str, ws: Path, base_hashes: dict, grader: g.Grader) -> None: + problems = [] + for dag_id in CONTEXT_PIPELINES: + res, _ = parse(py, ws, extra={"_AIRFLOW_PARSING_CONTEXT_DAG_ID": dag_id, + "_AIRFLOW_PARSING_CONTEXT_TASK_ID": "extract"}) + if "error" in res: + problems.append(f"{dag_id}: probe failed {res['error'][-300:]}") + continue + built = sorted(res["dags"]) + if built != [dag_id]: + problems.append(f"{dag_id}: built {len(built)} DAGs ({built[:3]}...)") + elif res["dags"][dag_id]["hash"] != base_hashes.get(dag_id): + problems.append(f"{dag_id}: differs from the DAG built in a full parse") + grader.primary("a worker parsing for one task builds only that task's DAG", not problems, "; ".join(problems)) + + +def check_runs(py: str, ws: Path, configs: dict, grader: g.Grader) -> None: + for dag_id in RUN_PIPELINES: + shutil.rmtree(ws / "output", ignore_errors=True) + run = g.run_dags_test(py, ws, dag_id, logical_date="2026-03-02T06:00:00+00:00", + env=g.airflow_env(ws), timeout=600) + detail = "" if run.ok else f"failed tasks={run.failed_tasks()}\n{run.log[-2500:]}" + problems = [] + out = ws / "output" / f"{dag_id}.jsonl" + if run.ok: + rows = [json.loads(line) for line in out.read_text().splitlines()] if out.exists() else [] + steps = [r.get("step") for r in rows] + cfg = configs[dag_id] + want = sorted(s["name"] for s in cfg["steps"]) + if sorted(steps) != want: + problems.append(f"steps run {steps}, expected each of {want} once") + pos = {s: i for i, s in enumerate(steps)} + for s in cfg["steps"]: + for u in s.get("depends_on", []): + if u in pos and s["name"] in pos and pos[u] > pos[s["name"]]: + problems.append(f"{s['name']} ran before {u}") + if any(not str(r.get("logical_date", "")).startswith("2026-03-02") for r in rows): + problems.append(f"logical dates {sorted({r.get('logical_date') for r in rows})}") + detail = "; ".join(problems) or f"{len(rows)} steps" + grader.primary(f"airflow dags test {dag_id} runs every step once in order", run.ok and not problems, detail) + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + configs = pristine_configs() + reg = registry() + + imp = g.import_dags(py, ws, env=g.airflow_env(ws), timeout=400) + grader.primary("DAG files import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + missing = sorted(set(configs) - set(imp["dags"])) + grader.primary("every pipeline config yields its DAG", not missing, + f"{len(missing)} of {len(configs)} missing: {missing[:6]}") + + parses = [] + for i, (seed, tz, shift) in enumerate(PARSE_SCENARIOS): + res, _ = parse(py, ws, details=("*",) if i == 0 else (), seed=seed, tz=tz, shift=shift) + parses.append(res) + errors = [p["error"] for p in parses if "error" in p] + if errors or missing: + grader.primary("every parse serializes each DAG the same way", False, + f"probe errors: {[e[-400:] for e in errors]}" if errors else "DAGs missing") + times = [p.get("parse_s") for p in parses] + grader.primary(f"factory parses in <= {PARSE_BUDGET_S}s", False, f"parse seconds={times}") + else: + unstable = sorted(n for n in configs if len({p["dags"].get(n, {}).get("hash") for p in parses}) > 1) + grader.primary("every parse serializes each DAG the same way", not unstable, + f"{len(unstable)} DAGs change between parses: {unstable[:6]}") + times = [p.get("parse_s") for p in parses] + # Median of the three parses, so one parse slowed by a busy grading machine does not decide it. + median = sorted(t if t is not None else float("inf") for t in times)[1] + grader.primary(f"factory parses in <= {PARSE_BUDGET_S}s", median <= PARSE_BUDGET_S, + f"parse seconds={times} (median {median})") + full = parses[0] + base_hashes = {n: d["hash"] for n, d in full["dags"].items()} + check_shapes(py, ws, full, configs, reg, grader) + check_isolation(py, ws, configs, grader) + check_parsing_context(py, ws, base_hashes, grader) + check_concurrency(py, ws, configs, grader) + check_runs(py, ws, configs, grader) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/dag_concurrency_not_pool/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/dag_concurrency_not_pool/dags/pipeline_factory.py new file mode 100644 index 0000000..a7f51de --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/dag_concurrency_not_pool/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_tasks=16, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/downstream_weight_rule/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/downstream_weight_rule/dags/pipeline_factory.py new file mode 100644 index 0000000..fa11f73 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/downstream_weight_rule/dags/pipeline_factory.py @@ -0,0 +1,139 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/index_minute/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/index_minute/dags/pipeline_factory.py new file mode 100644 index 0000000..108ecc5 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/index_minute/dags/pipeline_factory.py @@ -0,0 +1,141 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + names = sorted(p.stem for p in CONFIG_DIR.glob("*.yaml")) + return (names.index(name) * 7) % 60 if name in names else 0 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/minute_zero/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/minute_zero/dags/pipeline_factory.py new file mode 100644 index 0000000..0610548 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/minute_zero/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return 0 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_parsing_context/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_parsing_context/dags/pipeline_factory.py new file mode 100644 index 0000000..e788251 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_parsing_context/dags/pipeline_factory.py @@ -0,0 +1,138 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_validate_autoregister/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_validate_autoregister/dags/pipeline_factory.py new file mode 100644 index 0000000..41633b5 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/no_validate_autoregister/dags/pipeline_factory.py @@ -0,0 +1,138 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/now_doc_md/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/now_doc_md/dags/pipeline_factory.py new file mode 100644 index 0000000..d7a6309 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/now_doc_md/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}` on {datetime.now():%Y-%m-%d %H:%M}. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/outer_try/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/outer_try/dags/pipeline_factory.py new file mode 100644 index 0000000..5c2634e --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/outer_try/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + path = None + try: + for path in sorted(CONFIG_DIR.glob("*.yaml")): + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + globals()[dag.dag_id] = dag + except Exception: + log.exception("Stopped at pipeline config %s: it could not be built", path.name) + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/dags/pipeline_factory.py new file mode 100644 index 0000000..653d8a2 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/pools.json b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/pools.json new file mode 100644 index 0000000..bc85e99 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/pool_not_declared/pools.json @@ -0,0 +1,3 @@ +{ + "default_pool": {"slots": 128, "description": "Default pool", "include_deferred": false} +} diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/python_hash_minute/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/python_hash_minute/dags/pipeline_factory.py new file mode 100644 index 0000000..98dea88 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/python_hash_minute/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return hash(name) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/registry_per_pipeline_libyaml/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/registry_per_pipeline_libyaml/dags/pipeline_factory.py new file mode 100644 index 0000000..7685799 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/registry_per_pipeline_libyaml/dags/pipeline_factory.py @@ -0,0 +1,139 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/set_owner/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/set_owner/dags/pipeline_factory.py new file mode 100644 index 0000000..569a011 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/set_owner/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = set(team["owners"]) | set(cfg.get("owners", [])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/mutants/silent_skip/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/silent_skip/dags/pipeline_factory.py new file mode 100644 index 0000000..7bfb9fe --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/mutants/silent_skip/dags/pipeline_factory.py @@ -0,0 +1,139 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/reference/dags/pipeline_factory.py b/evals/airflow/cases/hard-reliability-factory-fleet/reference/dags/pipeline_factory.py new file mode 100644 index 0000000..653d8a2 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/reference/dags/pipeline_factory.py @@ -0,0 +1,140 @@ +"""Builds one DAG per pipeline config in configs/pipelines/*.yaml. + +Each config names its team (looked up in the platform registry for owners and +retry defaults), a schedule (hour and day-of-week; the start minute is spread +over the hour so the warehouse does not get every pipeline at :00) and a list +of steps. Every step becomes a task that runs `run_step`. + +Parsing rules this file keeps: +- the registry is read once per parse (with libyaml when available); +- a config that fails to load, build or validate is logged with its file + name and skipped, so it only takes out its own DAG; +- every value that ends up in a DAG comes from the configs, never from the + clock, the process (`hash()` is salted per process) or the set of other + configs, so each DAG serializes the same way on every parse; +- when a worker parses the file to run one task, only that task's DAG is built; +- every task runs in the `warehouse` pool (pools.json) and critical pipelines + outrank standard ones with absolute priority weights. +""" + +from __future__ import annotations + +import json +import logging +import zlib +from datetime import datetime, timedelta +from functools import lru_cache +from pathlib import Path + +import yaml +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, get_parsing_context + +log = logging.getLogger(__name__) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_DIR = PROJECT_ROOT / "configs" / "pipelines" +REGISTRY = PROJECT_ROOT / "configs" / "registry.yaml" +OUTPUT_DIR = PROJECT_ROOT / "output" +Loader = getattr(yaml, "CSafeLoader", yaml.SafeLoader) +WAREHOUSE_POOL = "warehouse" +PRIORITY = {"critical": 10, "standard": 1} + + +def run_step(pipeline: str, step: str, table: str, **context) -> str: + """Stand-in for the warehouse call: records which step ran for which logical date.""" + logical_date = context.get("logical_date") + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + record = { + "pipeline": pipeline, + "step": step, + "table": table, + "logical_date": logical_date.isoformat() if logical_date else None, + } + with open(OUTPUT_DIR / f"{pipeline}.jsonl", "a") as fh: + fh.write(json.dumps(record) + "\n") + print(f"{pipeline}.{step} -> {table}") + return table + + +def load_yaml(path: Path) -> dict: + with open(path) as fh: + return yaml.load(fh, Loader=Loader) + + +@lru_cache(maxsize=1) +def registry() -> dict: + return load_yaml(REGISTRY) + + +def start_minute(name: str) -> int: + # Stable across processes (unlike hash()) and independent of other configs. + return zlib.crc32(name.encode()) % 60 + + +def build_dag(path: Path, cfg: dict) -> DAG: + reg = registry() + team = reg["teams"][cfg["team"]] + sched = cfg["schedule"] + minute = sched.get("minute", start_minute(cfg["name"])) + owners = list(dict.fromkeys([*team["owners"], *cfg.get("owners", [])])) + + dag = DAG( + dag_id=cfg["name"], + description=cfg.get("description"), + schedule=f"{minute} {sched['hour']} * * {sched.get('days', '*')}", + start_date=datetime(2026, 1, 1), + catchup=False, + tags=[cfg["team"], "fleet"], + # Registered through globals() only once fully built and validated: + # with auto-registration a DAG that fails half-way would still load. + auto_register=False, + doc_md=f"Generated from `{path.name}`. On-call: {team['oncall']} ({team['slack']}).", + default_args={ + "owner": ",".join(owners), + "retries": cfg.get("retries", team["default_retries"]), + "retry_delay": timedelta(minutes=5), + # Every step is a warehouse query: share the fleet-wide warehouse pool. + "pool": WAREHOUSE_POOL, + # Absolute weights: the scheduler compares these as-is, so a critical + # task outranks every standard task whatever the size of either DAG + # (the default "downstream" rule sums weights over downstream tasks). + "priority_weight": PRIORITY[cfg.get("tier", "standard")], + "weight_rule": "absolute", + }, + ) + with dag: + tasks = {} + for step in cfg["steps"]: + if step["table"] not in reg["datasets"]: + raise ValueError(f"unknown table {step['table']!r}: register it in the platform catalog first") + tasks[step["name"]] = PythonOperator( + task_id=step["name"], + python_callable=run_step, + op_kwargs={"pipeline": cfg["name"], "step": step["name"], "table": step["table"]}, + ) + for step in cfg["steps"]: + for upstream in step.get("depends_on", []): + tasks[upstream] >> tasks[step["name"]] + # Checks the DagBag would otherwise run later for the whole file + # (an invalid cron expression, a dependency cycle), so they fail only this config. + dag.validate() + dag.check_cycle() + return dag + + +def build_all() -> None: + wanted = get_parsing_context().dag_id + for path in sorted(CONFIG_DIR.glob("*.yaml")): + try: + cfg = load_yaml(path) + if wanted and cfg.get("name") != wanted: + continue + dag = build_dag(path, cfg) + except Exception: + log.exception("Skipping pipeline config %s: it could not be built", path.name) + continue + globals()[dag.dag_id] = dag + + +build_all() diff --git a/evals/airflow/cases/hard-reliability-factory-fleet/reference/pools.json b/evals/airflow/cases/hard-reliability-factory-fleet/reference/pools.json new file mode 100644 index 0000000..dc37013 --- /dev/null +++ b/evals/airflow/cases/hard-reliability-factory-fleet/reference/pools.json @@ -0,0 +1,4 @@ +{ + "default_pool": {"slots": 128, "description": "Default pool", "include_deferred": false}, + "warehouse": {"slots": 16, "description": "Warehouse query slots shared by the whole fleet", "include_deferred": false} +} diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/alt_reference/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/alt_reference/dags/hourly_pageviews.py new file mode 100644 index 0000000..c3e0b95 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/alt_reference/dags/hourly_pageviews.py @@ -0,0 +1,98 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" +MARKERS = OUTPUT / "markers" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def _hour_key(dt: datetime) -> str: + return dt.strftime("%Y%m%dT%H") + + +@dag( + # Runs at the top of the hour; the data interval is the hour that just ended. + schedule=CronTriggerTimetable("0 * * * *", timezone="UTC", interval=timedelta(hours=1)), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + tags=["analytics", "hourly"], +) +def hourly_pageviews(): + @task + def extract_hour(data_interval_start=None, data_interval_end=None) -> int: + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{_hour_key(data_interval_start)}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if data_interval_start <= _parse(event["ts"]) < data_interval_end: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + @task + def aggregate_hour(data_interval_start=None) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{_hour_key(data_interval_start)}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + prev_file = HOURLY / f"{_hour_key(data_interval_start - timedelta(hours=1))}.csv" + previous: dict[str, int] = {} + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{_hour_key(data_interval_start)}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + @task + def publish_hour(staged: int, data_interval_start=None) -> None: + MARKERS.mkdir(parents=True, exist_ok=True) + marker = MARKERS / f"{data_interval_start.strftime('%Y%m%dT%H%M%S')}.done" + marker.write_text(f"hour={data_interval_start.strftime('%Y-%m-%dT%H:00')} staged={staged}\n") + day_before = (data_interval_start - timedelta(days=1)).strftime("%Y%m%d") + for stale in STAGING.glob(f"{day_before}T*.csv"): + stale.unlink() + + staged = extract_hour() + aggregated = aggregate_hour() + staged >> aggregated >> publish_hour(staged) + + +hourly_pageviews() diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/case.yaml b/evals/airflow/cases/migration-hourly-pageviews-templates/case.yaml new file mode 100644 index 0000000..02865eb --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/case.yaml @@ -0,0 +1,28 @@ +id: migration-hourly-pageviews-templates +area: migration +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + I ran ruff's Airflow rules on this project but I'm not confident that's all + the migration needs. Can you get the hourly_pageviews DAG ready for Airflow + 3.3? The venv here already has apache-airflow 3.3.2. The dashboard loader + depends on the hourly files and the .done markers exactly as described in the + README, so their names and contents can't change. +description: | + Airflow 2.11 -> 3.3 migration of an hourly rollup whose behaviour lives in + Jinja templates. Pain point P7 (migration beyond ruff) and P2 (schedule + semantics). Ruff AIR flags only the imports and `schedule_interval`; it does + not look inside templated `op_kwargs`/`bash_command`, which use removed + context variables `{{ execution_date }}`, `{{ next_execution_date }}`, + `{{ prev_execution_date }}` and `{{ yesterday_ds_nodash }}` (render-time + failures on 3.3). The bare cron "0 * * * *" becomes CronTriggerTimetable in + Airflow 3, so a straight `execution_date` -> `logical_date` rename processes + the hour after the tick, and the midnight run's staging cleanup deletes the + wrong day. Primary: imports on 3.3, tasks and order preserved, dags test + succeeds, three scheduled runs (10:00, 11:00 and the 00:00 day rollover) + replayed in 3.3 from the same seeded staging area write the same hourly + rollups, the same .done markers and leave the same staging files as the + original replayed in 2.11. Secondary: verification commands, ruff AIR + clean, no deprecation warnings. diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/.gitignore b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/README.md b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/README.md new file mode 100644 index 0000000..8fa482a --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/README.md @@ -0,0 +1,13 @@ +# product-analytics + +`dags/hourly_pageviews.py` rolls up raw pageview events (`data/pageviews.jsonl`, +UTC timestamps) once per clock hour. + +| Path | Contents | Consumer | +|---|---|---| +| `output/hourly/.csv` | views, unique sessions and change vs the previous hour, per page, for the hour starting at `HH:00` | analytics dashboard | +| `output/markers/.done` | `hour=... staged=`; the dashboard loader waits for it | dashboard loader | +| `output/staging/.csv` | staged raw events for the hour; kept for about a day | debugging / replays | + +The run at 10:00 UTC processes 09:00-10:00 and writes `20260305T09.csv` and +`20260305T090000.done`. diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/dags/hourly_pageviews.py new file mode 100644 index 0000000..dbbc409 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/dags/hourly_pageviews.py @@ -0,0 +1,110 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.operators.bash import BashOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + schedule_interval="0 * * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract = PythonOperator( + task_id="extract_hour", + python_callable=extract_hour, + op_kwargs={ + "start": "{{ execution_date }}", + "end": "{{ next_execution_date }}", + "hour": "{{ execution_date.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate = PythonOperator( + task_id="aggregate_hour", + python_callable=aggregate_hour, + op_kwargs={ + "hour": "{{ execution_date.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ prev_execution_date.strftime('%Y%m%dT%H') }}", + }, + ) + publish = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ execution_date.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ yesterday_ds_nodash }}T*.csv' -delete" + ), + ) + extract >> aggregate >> publish diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/data/pageviews.jsonl b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/data/pageviews.jsonl new file mode 100644 index 0000000..afcf0cc --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/data/pageviews.jsonl @@ -0,0 +1,787 @@ +{"ts": "2026-03-05T08:00:57Z", "session_id": "s004", "page": "/docs"} +{"ts": "2026-03-05T08:01:34Z", "session_id": "s027", "page": "/docs"} +{"ts": "2026-03-05T08:03:01Z", "session_id": "s006", "page": "/signup"} +{"ts": "2026-03-05T08:03:35Z", "session_id": "s085", "page": "/docs"} +{"ts": "2026-03-05T08:05:34Z", "session_id": "s053", "page": "/blog"} +{"ts": "2026-03-05T08:07:44Z", "session_id": "s076", "page": "/checkout"} +{"ts": "2026-03-05T08:08:22Z", "session_id": "s085", "page": "/signup"} +{"ts": "2026-03-05T08:08:49Z", "session_id": "s014", "page": "/pricing"} +{"ts": "2026-03-05T08:09:21Z", "session_id": "s062", "page": "/docs"} +{"ts": "2026-03-05T08:10:17Z", "session_id": "s074", "page": "/signup"} +{"ts": "2026-03-05T08:12:36Z", "session_id": "s038", "page": "/"} +{"ts": "2026-03-05T08:13:29Z", "session_id": "s037", "page": "/docs"} +{"ts": "2026-03-05T08:15:07Z", "session_id": "s084", "page": "/blog"} +{"ts": "2026-03-05T08:17:16Z", "session_id": "s072", "page": "/checkout"} +{"ts": "2026-03-05T08:19:12Z", "session_id": "s009", "page": "/docs"} +{"ts": "2026-03-05T08:21:14Z", "session_id": "s082", "page": "/"} +{"ts": "2026-03-05T08:22:24Z", "session_id": "s061", "page": "/checkout"} +{"ts": "2026-03-05T08:24:35Z", "session_id": "s040", "page": "/pricing"} +{"ts": "2026-03-05T08:25:36Z", "session_id": "s072", "page": "/signup"} +{"ts": "2026-03-05T08:27:52Z", "session_id": "s078", "page": "/checkout"} +{"ts": "2026-03-05T08:28:34Z", "session_id": "s073", "page": "/pricing"} +{"ts": "2026-03-05T08:29:54Z", "session_id": "s064", "page": "/checkout"} +{"ts": "2026-03-05T08:32:13Z", "session_id": "s076", "page": "/pricing"} +{"ts": "2026-03-05T08:33:33Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T08:35:20Z", "session_id": "s066", "page": "/pricing"} +{"ts": "2026-03-05T08:36:03Z", "session_id": "s050", "page": "/checkout"} +{"ts": "2026-03-05T08:36:38Z", "session_id": "s082", "page": "/docs"} +{"ts": "2026-03-05T08:37:39Z", "session_id": "s010", "page": "/pricing"} +{"ts": "2026-03-05T08:38:36Z", "session_id": "s021", "page": "/"} +{"ts": "2026-03-05T08:40:49Z", "session_id": "s083", "page": "/pricing"} +{"ts": "2026-03-05T08:43:08Z", "session_id": "s042", "page": "/pricing"} +{"ts": "2026-03-05T08:44:16Z", "session_id": "s019", "page": "/checkout"} +{"ts": "2026-03-05T08:45:26Z", "session_id": "s008", "page": "/pricing"} +{"ts": "2026-03-05T08:46:10Z", "session_id": "s048", "page": "/checkout"} +{"ts": "2026-03-05T08:47:18Z", "session_id": "s060", "page": "/pricing"} +{"ts": "2026-03-05T08:47:49Z", "session_id": "s017", "page": "/docs"} +{"ts": "2026-03-05T08:48:47Z", "session_id": "s023", "page": "/"} +{"ts": "2026-03-05T08:49:39Z", "session_id": "s038", "page": "/checkout"} +{"ts": "2026-03-05T08:51:32Z", "session_id": "s044", "page": "/signup"} +{"ts": "2026-03-05T08:53:36Z", "session_id": "s062", "page": "/blog"} +{"ts": "2026-03-05T08:55:00Z", "session_id": "s056", "page": "/signup"} +{"ts": "2026-03-05T08:56:58Z", "session_id": "s027", "page": "/"} +{"ts": "2026-03-05T08:59:11Z", "session_id": "s060", "page": "/docs"} +{"ts": "2026-03-05T09:00:00Z", "session_id": "s999", "page": "/"} +{"ts": "2026-03-05T09:00:52Z", "session_id": "s002", "page": "/blog"} +{"ts": "2026-03-05T09:01:36Z", "session_id": "s077", "page": "/signup"} +{"ts": "2026-03-05T09:02:35Z", "session_id": "s057", "page": "/docs"} +{"ts": "2026-03-05T09:02:59Z", "session_id": "s005", "page": "/docs"} +{"ts": "2026-03-05T09:03:51Z", "session_id": "s050", "page": "/pricing"} +{"ts": "2026-03-05T09:04:55Z", "session_id": "s023", "page": "/pricing"} +{"ts": "2026-03-05T09:05:57Z", "session_id": "s063", "page": "/signup"} +{"ts": "2026-03-05T09:06:47Z", "session_id": "s077", "page": "/pricing"} +{"ts": "2026-03-05T09:07:40Z", "session_id": "s020", "page": "/checkout"} +{"ts": "2026-03-05T09:08:23Z", "session_id": "s025", "page": "/pricing"} +{"ts": "2026-03-05T09:10:17Z", "session_id": "s050", "page": "/signup"} +{"ts": "2026-03-05T09:11:35Z", "session_id": "s044", "page": "/signup"} +{"ts": "2026-03-05T09:13:36Z", "session_id": "s088", "page": "/checkout"} +{"ts": "2026-03-05T09:14:05Z", "session_id": "s011", "page": "/checkout"} +{"ts": "2026-03-05T09:15:13Z", "session_id": "s086", "page": "/blog"} +{"ts": "2026-03-05T09:16:46Z", "session_id": "s053", "page": "/signup"} +{"ts": "2026-03-05T09:18:25Z", "session_id": "s061", "page": "/docs"} +{"ts": "2026-03-05T09:20:09Z", "session_id": "s036", "page": "/docs"} +{"ts": "2026-03-05T09:21:00Z", "session_id": "s029", "page": "/checkout"} +{"ts": "2026-03-05T09:21:42Z", "session_id": "s082", "page": "/signup"} +{"ts": "2026-03-05T09:22:28Z", "session_id": "s031", "page": "/blog"} +{"ts": "2026-03-05T09:22:51Z", "session_id": "s033", "page": "/signup"} +{"ts": "2026-03-05T09:24:39Z", "session_id": "s089", "page": "/pricing"} +{"ts": "2026-03-05T09:26:11Z", "session_id": "s064", "page": "/checkout"} +{"ts": "2026-03-05T09:28:21Z", "session_id": "s012", "page": "/docs"} +{"ts": "2026-03-05T09:29:48Z", "session_id": "s073", "page": "/"} +{"ts": "2026-03-05T09:31:13Z", "session_id": "s007", "page": "/pricing"} +{"ts": "2026-03-05T09:31:38Z", "session_id": "s082", "page": "/"} +{"ts": "2026-03-05T09:33:53Z", "session_id": "s049", "page": "/pricing"} +{"ts": "2026-03-05T09:34:46Z", "session_id": "s049", "page": "/checkout"} +{"ts": "2026-03-05T09:35:56Z", "session_id": "s011", "page": "/pricing"} +{"ts": "2026-03-05T09:36:46Z", "session_id": "s086", "page": "/pricing"} +{"ts": "2026-03-05T09:38:27Z", "session_id": "s035", "page": "/signup"} +{"ts": "2026-03-05T09:39:07Z", "session_id": "s062", "page": "/"} +{"ts": "2026-03-05T09:39:33Z", "session_id": "s051", "page": "/docs"} +{"ts": "2026-03-05T09:41:52Z", "session_id": "s050", "page": "/blog"} +{"ts": "2026-03-05T09:42:44Z", "session_id": "s081", "page": "/checkout"} +{"ts": "2026-03-05T09:44:54Z", "session_id": "s018", "page": "/signup"} +{"ts": "2026-03-05T09:46:00Z", "session_id": "s075", "page": "/blog"} +{"ts": "2026-03-05T09:46:23Z", "session_id": "s068", "page": "/"} +{"ts": "2026-03-05T09:47:15Z", "session_id": "s036", "page": "/docs"} +{"ts": "2026-03-05T09:48:04Z", "session_id": "s008", "page": "/blog"} +{"ts": "2026-03-05T09:48:58Z", "session_id": "s067", "page": "/checkout"} +{"ts": "2026-03-05T09:50:25Z", "session_id": "s088", "page": "/docs"} +{"ts": "2026-03-05T09:50:48Z", "session_id": "s072", "page": "/blog"} +{"ts": "2026-03-05T09:51:29Z", "session_id": "s030", "page": "/blog"} +{"ts": "2026-03-05T09:52:15Z", "session_id": "s042", "page": "/signup"} +{"ts": "2026-03-05T09:53:22Z", "session_id": "s077", "page": "/checkout"} +{"ts": "2026-03-05T09:55:39Z", "session_id": "s009", "page": "/blog"} +{"ts": "2026-03-05T09:56:00Z", "session_id": "s045", "page": "/signup"} +{"ts": "2026-03-05T09:57:57Z", "session_id": "s010", "page": "/checkout"} +{"ts": "2026-03-05T09:58:29Z", "session_id": "s053", "page": "/pricing"} +{"ts": "2026-03-05T10:00:00Z", "session_id": "s999", "page": "/"} +{"ts": "2026-03-05T10:00:14Z", "session_id": "s084", "page": "/blog"} +{"ts": "2026-03-05T10:00:59Z", "session_id": "s081", "page": "/"} +{"ts": "2026-03-05T10:02:31Z", "session_id": "s065", "page": "/checkout"} +{"ts": "2026-03-05T10:02:51Z", "session_id": "s053", "page": "/signup"} +{"ts": "2026-03-05T10:04:16Z", "session_id": "s044", "page": "/checkout"} +{"ts": "2026-03-05T10:04:45Z", "session_id": "s009", "page": "/pricing"} +{"ts": "2026-03-05T10:06:22Z", "session_id": "s060", "page": "/checkout"} +{"ts": "2026-03-05T10:06:44Z", "session_id": "s019", "page": "/checkout"} +{"ts": "2026-03-05T10:08:36Z", "session_id": "s012", "page": "/checkout"} +{"ts": "2026-03-05T10:10:46Z", "session_id": "s069", "page": "/signup"} +{"ts": "2026-03-05T10:12:29Z", "session_id": "s016", "page": "/blog"} +{"ts": "2026-03-05T10:14:48Z", "session_id": "s008", "page": "/pricing"} +{"ts": "2026-03-05T10:16:22Z", "session_id": "s070", "page": "/pricing"} +{"ts": "2026-03-05T10:17:06Z", "session_id": "s016", "page": "/checkout"} +{"ts": "2026-03-05T10:18:05Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T10:19:34Z", "session_id": "s041", "page": "/signup"} +{"ts": "2026-03-05T10:21:20Z", "session_id": "s064", "page": "/signup"} +{"ts": "2026-03-05T10:21:40Z", "session_id": "s079", "page": "/docs"} +{"ts": "2026-03-05T10:22:53Z", "session_id": "s008", "page": "/"} +{"ts": "2026-03-05T10:23:54Z", "session_id": "s028", "page": "/blog"} +{"ts": "2026-03-05T10:24:41Z", "session_id": "s008", "page": "/pricing"} +{"ts": "2026-03-05T10:26:29Z", "session_id": "s074", "page": "/pricing"} +{"ts": "2026-03-05T10:27:16Z", "session_id": "s011", "page": "/pricing"} +{"ts": "2026-03-05T10:28:46Z", "session_id": "s083", "page": "/"} +{"ts": "2026-03-05T10:30:02Z", "session_id": "s076", "page": "/checkout"} +{"ts": "2026-03-05T10:30:41Z", "session_id": "s007", "page": "/"} +{"ts": "2026-03-05T10:32:42Z", "session_id": "s079", "page": "/"} +{"ts": "2026-03-05T10:34:53Z", "session_id": "s067", "page": "/pricing"} +{"ts": "2026-03-05T10:35:28Z", "session_id": "s034", "page": "/signup"} +{"ts": "2026-03-05T10:37:03Z", "session_id": "s077", "page": "/blog"} +{"ts": "2026-03-05T10:38:23Z", "session_id": "s006", "page": "/blog"} +{"ts": "2026-03-05T10:39:32Z", "session_id": "s073", "page": "/"} +{"ts": "2026-03-05T10:40:47Z", "session_id": "s016", "page": "/blog"} +{"ts": "2026-03-05T10:41:50Z", "session_id": "s067", "page": "/"} +{"ts": "2026-03-05T10:42:23Z", "session_id": "s043", "page": "/checkout"} +{"ts": "2026-03-05T10:44:26Z", "session_id": "s082", "page": "/docs"} +{"ts": "2026-03-05T10:44:54Z", "session_id": "s089", "page": "/"} +{"ts": "2026-03-05T10:46:17Z", "session_id": "s071", "page": "/checkout"} +{"ts": "2026-03-05T10:47:33Z", "session_id": "s056", "page": "/signup"} +{"ts": "2026-03-05T10:48:40Z", "session_id": "s017", "page": "/pricing"} +{"ts": "2026-03-05T10:49:31Z", "session_id": "s072", "page": "/docs"} +{"ts": "2026-03-05T10:50:16Z", "session_id": "s074", "page": "/checkout"} +{"ts": "2026-03-05T10:51:50Z", "session_id": "s059", "page": "/docs"} +{"ts": "2026-03-05T10:53:59Z", "session_id": "s067", "page": "/blog"} +{"ts": "2026-03-05T10:55:11Z", "session_id": "s060", "page": "/checkout"} +{"ts": "2026-03-05T10:57:23Z", "session_id": "s034", "page": "/pricing"} +{"ts": "2026-03-05T10:58:30Z", "session_id": "s014", "page": "/blog"} +{"ts": "2026-03-05T11:00:00Z", "session_id": "s999", "page": "/"} +{"ts": "2026-03-05T11:00:47Z", "session_id": "s044", "page": "/checkout"} +{"ts": "2026-03-05T11:03:05Z", "session_id": "s069", "page": "/signup"} +{"ts": "2026-03-05T11:04:46Z", "session_id": "s020", "page": "/pricing"} +{"ts": "2026-03-05T11:05:20Z", "session_id": "s061", "page": "/"} +{"ts": "2026-03-05T11:07:04Z", "session_id": "s076", "page": "/checkout"} +{"ts": "2026-03-05T11:07:44Z", "session_id": "s087", "page": "/pricing"} +{"ts": "2026-03-05T11:09:51Z", "session_id": "s051", "page": "/"} +{"ts": "2026-03-05T11:11:21Z", "session_id": "s036", "page": "/blog"} +{"ts": "2026-03-05T11:13:19Z", "session_id": "s059", "page": "/blog"} +{"ts": "2026-03-05T11:14:56Z", "session_id": "s016", "page": "/"} +{"ts": "2026-03-05T11:16:31Z", "session_id": "s089", "page": "/pricing"} +{"ts": "2026-03-05T11:17:18Z", "session_id": "s047", "page": "/docs"} +{"ts": "2026-03-05T11:17:45Z", "session_id": "s022", "page": "/docs"} +{"ts": "2026-03-05T11:19:34Z", "session_id": "s031", "page": "/pricing"} +{"ts": "2026-03-05T11:21:34Z", "session_id": "s066", "page": "/checkout"} +{"ts": "2026-03-05T11:23:32Z", "session_id": "s088", "page": "/"} +{"ts": "2026-03-05T11:24:47Z", "session_id": "s088", "page": "/signup"} +{"ts": "2026-03-05T11:26:12Z", "session_id": "s077", "page": "/pricing"} +{"ts": "2026-03-05T11:28:16Z", "session_id": "s021", "page": "/pricing"} +{"ts": "2026-03-05T11:30:28Z", "session_id": "s074", "page": "/checkout"} +{"ts": "2026-03-05T11:31:33Z", "session_id": "s049", "page": "/docs"} +{"ts": "2026-03-05T11:33:19Z", "session_id": "s049", "page": "/signup"} +{"ts": "2026-03-05T11:35:00Z", "session_id": "s040", "page": "/blog"} +{"ts": "2026-03-05T11:35:20Z", "session_id": "s081", "page": "/blog"} +{"ts": "2026-03-05T11:37:28Z", "session_id": "s046", "page": "/blog"} +{"ts": "2026-03-05T11:38:30Z", "session_id": "s013", "page": "/docs"} +{"ts": "2026-03-05T11:39:32Z", "session_id": "s007", "page": "/checkout"} +{"ts": "2026-03-05T11:41:28Z", "session_id": "s056", "page": "/blog"} +{"ts": "2026-03-05T11:42:12Z", "session_id": "s032", "page": "/"} +{"ts": "2026-03-05T11:42:53Z", "session_id": "s011", "page": "/checkout"} +{"ts": "2026-03-05T11:44:59Z", "session_id": "s080", "page": "/docs"} +{"ts": "2026-03-05T11:45:38Z", "session_id": "s030", "page": "/"} +{"ts": "2026-03-05T11:47:48Z", "session_id": "s089", "page": "/"} +{"ts": "2026-03-05T11:48:29Z", "session_id": "s068", "page": "/blog"} +{"ts": "2026-03-05T11:48:59Z", "session_id": "s014", "page": "/pricing"} +{"ts": "2026-03-05T11:49:52Z", "session_id": "s024", "page": "/pricing"} +{"ts": "2026-03-05T11:52:01Z", "session_id": "s027", "page": "/signup"} +{"ts": "2026-03-05T11:54:17Z", "session_id": "s084", "page": "/signup"} +{"ts": "2026-03-05T11:56:00Z", "session_id": "s014", "page": "/checkout"} +{"ts": "2026-03-05T11:56:28Z", "session_id": "s079", "page": "/pricing"} +{"ts": "2026-03-05T11:57:44Z", "session_id": "s002", "page": "/pricing"} +{"ts": "2026-03-05T11:58:52Z", "session_id": "s024", "page": "/signup"} +{"ts": "2026-03-05T12:01:03Z", "session_id": "s069", "page": "/"} +{"ts": "2026-03-05T12:03:12Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T12:04:39Z", "session_id": "s057", "page": "/pricing"} +{"ts": "2026-03-05T12:05:07Z", "session_id": "s034", "page": "/"} +{"ts": "2026-03-05T12:06:20Z", "session_id": "s014", "page": "/signup"} +{"ts": "2026-03-05T12:07:23Z", "session_id": "s010", "page": "/signup"} +{"ts": "2026-03-05T12:09:18Z", "session_id": "s047", "page": "/blog"} +{"ts": "2026-03-05T12:09:49Z", "session_id": "s082", "page": "/"} +{"ts": "2026-03-05T12:11:29Z", "session_id": "s075", "page": "/checkout"} +{"ts": "2026-03-05T12:13:38Z", "session_id": "s066", "page": "/pricing"} +{"ts": "2026-03-05T12:14:28Z", "session_id": "s076", "page": "/checkout"} +{"ts": "2026-03-05T12:15:51Z", "session_id": "s012", "page": "/docs"} +{"ts": "2026-03-05T12:16:52Z", "session_id": "s013", "page": "/blog"} +{"ts": "2026-03-05T12:17:22Z", "session_id": "s016", "page": "/blog"} +{"ts": "2026-03-05T12:17:42Z", "session_id": "s081", "page": "/docs"} +{"ts": "2026-03-05T12:18:36Z", "session_id": "s071", "page": "/blog"} +{"ts": "2026-03-05T12:19:24Z", "session_id": "s035", "page": "/blog"} +{"ts": "2026-03-05T12:20:45Z", "session_id": "s045", "page": "/docs"} +{"ts": "2026-03-05T12:22:39Z", "session_id": "s022", "page": "/"} +{"ts": "2026-03-05T12:24:28Z", "session_id": "s085", "page": "/checkout"} +{"ts": "2026-03-05T12:26:42Z", "session_id": "s063", "page": "/pricing"} +{"ts": "2026-03-05T12:27:06Z", "session_id": "s067", "page": "/pricing"} +{"ts": "2026-03-05T12:28:29Z", "session_id": "s005", "page": "/pricing"} +{"ts": "2026-03-05T12:29:41Z", "session_id": "s047", "page": "/"} +{"ts": "2026-03-05T12:31:42Z", "session_id": "s058", "page": "/pricing"} +{"ts": "2026-03-05T12:32:46Z", "session_id": "s041", "page": "/checkout"} +{"ts": "2026-03-05T12:34:59Z", "session_id": "s071", "page": "/pricing"} +{"ts": "2026-03-05T12:37:16Z", "session_id": "s059", "page": "/checkout"} +{"ts": "2026-03-05T12:38:18Z", "session_id": "s017", "page": "/signup"} +{"ts": "2026-03-05T12:38:55Z", "session_id": "s034", "page": "/"} +{"ts": "2026-03-05T12:40:19Z", "session_id": "s084", "page": "/docs"} +{"ts": "2026-03-05T12:41:21Z", "session_id": "s020", "page": "/signup"} +{"ts": "2026-03-05T12:43:18Z", "session_id": "s030", "page": "/signup"} +{"ts": "2026-03-05T12:45:36Z", "session_id": "s004", "page": "/checkout"} +{"ts": "2026-03-05T12:46:49Z", "session_id": "s037", "page": "/blog"} +{"ts": "2026-03-05T12:47:16Z", "session_id": "s006", "page": "/checkout"} +{"ts": "2026-03-05T12:48:51Z", "session_id": "s026", "page": "/checkout"} +{"ts": "2026-03-05T12:51:10Z", "session_id": "s016", "page": "/docs"} +{"ts": "2026-03-05T12:52:30Z", "session_id": "s035", "page": "/docs"} +{"ts": "2026-03-05T12:53:30Z", "session_id": "s062", "page": "/signup"} +{"ts": "2026-03-05T12:54:30Z", "session_id": "s082", "page": "/signup"} +{"ts": "2026-03-05T12:55:00Z", "session_id": "s006", "page": "/blog"} +{"ts": "2026-03-05T12:56:44Z", "session_id": "s025", "page": "/blog"} +{"ts": "2026-03-05T12:57:32Z", "session_id": "s080", "page": "/"} +{"ts": "2026-03-05T12:58:26Z", "session_id": "s072", "page": "/docs"} +{"ts": "2026-03-05T12:59:58Z", "session_id": "s049", "page": "/checkout"} +{"ts": "2026-03-05T13:01:49Z", "session_id": "s032", "page": "/checkout"} +{"ts": "2026-03-05T13:02:22Z", "session_id": "s058", "page": "/pricing"} +{"ts": "2026-03-05T13:03:08Z", "session_id": "s006", "page": "/checkout"} +{"ts": "2026-03-05T13:05:02Z", "session_id": "s061", "page": "/blog"} +{"ts": "2026-03-05T13:05:30Z", "session_id": "s006", "page": "/pricing"} +{"ts": "2026-03-05T13:07:46Z", "session_id": "s084", "page": "/"} +{"ts": "2026-03-05T13:08:12Z", "session_id": "s057", "page": "/signup"} +{"ts": "2026-03-05T13:08:52Z", "session_id": "s047", "page": "/checkout"} +{"ts": "2026-03-05T13:09:37Z", "session_id": "s022", "page": "/checkout"} +{"ts": "2026-03-05T13:11:02Z", "session_id": "s087", "page": "/signup"} +{"ts": "2026-03-05T13:11:50Z", "session_id": "s006", "page": "/checkout"} +{"ts": "2026-03-05T13:12:21Z", "session_id": "s074", "page": "/signup"} +{"ts": "2026-03-05T13:12:58Z", "session_id": "s063", "page": "/"} +{"ts": "2026-03-05T13:13:20Z", "session_id": "s087", "page": "/pricing"} +{"ts": "2026-03-05T13:14:58Z", "session_id": "s066", "page": "/"} +{"ts": "2026-03-05T13:16:30Z", "session_id": "s085", "page": "/"} +{"ts": "2026-03-05T13:17:34Z", "session_id": "s059", "page": "/blog"} +{"ts": "2026-03-05T13:19:25Z", "session_id": "s061", "page": "/pricing"} +{"ts": "2026-03-05T13:21:00Z", "session_id": "s012", "page": "/docs"} +{"ts": "2026-03-05T13:23:07Z", "session_id": "s025", "page": "/docs"} +{"ts": "2026-03-05T13:24:03Z", "session_id": "s050", "page": "/docs"} +{"ts": "2026-03-05T13:25:31Z", "session_id": "s049", "page": "/pricing"} +{"ts": "2026-03-05T13:27:28Z", "session_id": "s028", "page": "/pricing"} +{"ts": "2026-03-05T13:28:00Z", "session_id": "s048", "page": "/docs"} +{"ts": "2026-03-05T13:28:48Z", "session_id": "s001", "page": "/pricing"} +{"ts": "2026-03-05T13:29:13Z", "session_id": "s021", "page": "/checkout"} +{"ts": "2026-03-05T13:31:17Z", "session_id": "s067", "page": "/docs"} +{"ts": "2026-03-05T13:31:46Z", "session_id": "s030", "page": "/pricing"} +{"ts": "2026-03-05T13:32:21Z", "session_id": "s084", "page": "/docs"} +{"ts": "2026-03-05T13:33:12Z", "session_id": "s078", "page": "/blog"} +{"ts": "2026-03-05T13:34:00Z", "session_id": "s007", "page": "/signup"} +{"ts": "2026-03-05T13:35:56Z", "session_id": "s023", "page": "/checkout"} +{"ts": "2026-03-05T13:38:03Z", "session_id": "s087", "page": "/blog"} +{"ts": "2026-03-05T13:40:21Z", "session_id": "s073", "page": "/pricing"} +{"ts": "2026-03-05T13:41:03Z", "session_id": "s052", "page": "/docs"} +{"ts": "2026-03-05T13:42:21Z", "session_id": "s078", "page": "/pricing"} +{"ts": "2026-03-05T13:42:49Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T13:43:22Z", "session_id": "s081", "page": "/docs"} +{"ts": "2026-03-05T13:43:43Z", "session_id": "s028", "page": "/"} +{"ts": "2026-03-05T13:44:35Z", "session_id": "s009", "page": "/"} +{"ts": "2026-03-05T13:45:09Z", "session_id": "s051", "page": "/docs"} +{"ts": "2026-03-05T13:46:19Z", "session_id": "s059", "page": "/docs"} +{"ts": "2026-03-05T13:47:52Z", "session_id": "s086", "page": "/pricing"} +{"ts": "2026-03-05T13:49:05Z", "session_id": "s077", "page": "/"} +{"ts": "2026-03-05T13:50:04Z", "session_id": "s007", "page": "/pricing"} +{"ts": "2026-03-05T13:50:34Z", "session_id": "s032", "page": "/blog"} +{"ts": "2026-03-05T13:52:20Z", "session_id": "s036", "page": "/signup"} +{"ts": "2026-03-05T13:53:56Z", "session_id": "s011", "page": "/pricing"} +{"ts": "2026-03-05T13:55:44Z", "session_id": "s060", "page": "/"} +{"ts": "2026-03-05T13:56:04Z", "session_id": "s088", "page": "/docs"} +{"ts": "2026-03-05T13:57:06Z", "session_id": "s041", "page": "/blog"} +{"ts": "2026-03-05T13:58:50Z", "session_id": "s089", "page": "/pricing"} +{"ts": "2026-03-05T14:00:16Z", "session_id": "s013", "page": "/docs"} +{"ts": "2026-03-05T14:00:55Z", "session_id": "s055", "page": "/docs"} +{"ts": "2026-03-05T14:01:15Z", "session_id": "s067", "page": "/blog"} +{"ts": "2026-03-05T14:01:56Z", "session_id": "s054", "page": "/docs"} +{"ts": "2026-03-05T14:03:57Z", "session_id": "s018", "page": "/pricing"} +{"ts": "2026-03-05T14:05:15Z", "session_id": "s088", "page": "/docs"} +{"ts": "2026-03-05T14:06:11Z", "session_id": "s031", "page": "/docs"} +{"ts": "2026-03-05T14:06:48Z", "session_id": "s015", "page": "/blog"} +{"ts": "2026-03-05T14:07:28Z", "session_id": "s073", "page": "/checkout"} +{"ts": "2026-03-05T14:08:02Z", "session_id": "s086", "page": "/"} +{"ts": "2026-03-05T14:08:39Z", "session_id": "s045", "page": "/pricing"} +{"ts": "2026-03-05T14:10:36Z", "session_id": "s036", "page": "/"} +{"ts": "2026-03-05T14:11:53Z", "session_id": "s025", "page": "/"} +{"ts": "2026-03-05T14:13:23Z", "session_id": "s024", "page": "/blog"} +{"ts": "2026-03-05T14:14:01Z", "session_id": "s016", "page": "/docs"} +{"ts": "2026-03-05T14:15:00Z", "session_id": "s066", "page": "/pricing"} +{"ts": "2026-03-05T14:17:01Z", "session_id": "s077", "page": "/signup"} +{"ts": "2026-03-05T14:18:35Z", "session_id": "s027", "page": "/checkout"} +{"ts": "2026-03-05T14:20:18Z", "session_id": "s030", "page": "/docs"} +{"ts": "2026-03-05T14:21:52Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T14:22:28Z", "session_id": "s026", "page": "/signup"} +{"ts": "2026-03-05T14:23:53Z", "session_id": "s089", "page": "/docs"} +{"ts": "2026-03-05T14:24:27Z", "session_id": "s055", "page": "/docs"} +{"ts": "2026-03-05T14:26:44Z", "session_id": "s031", "page": "/checkout"} +{"ts": "2026-03-05T14:27:50Z", "session_id": "s007", "page": "/signup"} +{"ts": "2026-03-05T14:29:13Z", "session_id": "s014", "page": "/pricing"} +{"ts": "2026-03-05T14:31:18Z", "session_id": "s041", "page": "/"} +{"ts": "2026-03-05T14:32:36Z", "session_id": "s068", "page": "/checkout"} +{"ts": "2026-03-05T14:34:37Z", "session_id": "s021", "page": "/pricing"} +{"ts": "2026-03-05T14:36:32Z", "session_id": "s030", "page": "/docs"} +{"ts": "2026-03-05T14:38:32Z", "session_id": "s088", "page": "/pricing"} +{"ts": "2026-03-05T14:40:02Z", "session_id": "s035", "page": "/blog"} +{"ts": "2026-03-05T14:41:06Z", "session_id": "s035", "page": "/"} +{"ts": "2026-03-05T14:41:34Z", "session_id": "s062", "page": "/"} +{"ts": "2026-03-05T14:43:10Z", "session_id": "s088", "page": "/"} +{"ts": "2026-03-05T14:45:24Z", "session_id": "s072", "page": "/checkout"} +{"ts": "2026-03-05T14:46:51Z", "session_id": "s006", "page": "/docs"} +{"ts": "2026-03-05T14:49:01Z", "session_id": "s018", "page": "/blog"} +{"ts": "2026-03-05T14:50:17Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T14:51:12Z", "session_id": "s057", "page": "/"} +{"ts": "2026-03-05T14:52:40Z", "session_id": "s044", "page": "/blog"} +{"ts": "2026-03-05T14:53:54Z", "session_id": "s020", "page": "/blog"} +{"ts": "2026-03-05T14:54:25Z", "session_id": "s082", "page": "/signup"} +{"ts": "2026-03-05T14:55:28Z", "session_id": "s018", "page": "/blog"} +{"ts": "2026-03-05T14:56:45Z", "session_id": "s059", "page": "/docs"} +{"ts": "2026-03-05T14:58:19Z", "session_id": "s010", "page": "/signup"} +{"ts": "2026-03-05T14:59:50Z", "session_id": "s067", "page": "/"} +{"ts": "2026-03-05T15:01:19Z", "session_id": "s005", "page": "/docs"} +{"ts": "2026-03-05T15:01:47Z", "session_id": "s029", "page": "/blog"} +{"ts": "2026-03-05T15:03:31Z", "session_id": "s004", "page": "/blog"} +{"ts": "2026-03-05T15:05:31Z", "session_id": "s086", "page": "/checkout"} +{"ts": "2026-03-05T15:06:25Z", "session_id": "s023", "page": "/blog"} +{"ts": "2026-03-05T15:07:25Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T15:08:50Z", "session_id": "s035", "page": "/blog"} +{"ts": "2026-03-05T15:10:42Z", "session_id": "s056", "page": "/pricing"} +{"ts": "2026-03-05T15:11:24Z", "session_id": "s002", "page": "/docs"} +{"ts": "2026-03-05T15:13:21Z", "session_id": "s022", "page": "/blog"} +{"ts": "2026-03-05T15:14:55Z", "session_id": "s007", "page": "/pricing"} +{"ts": "2026-03-05T15:15:25Z", "session_id": "s001", "page": "/"} +{"ts": "2026-03-05T15:17:03Z", "session_id": "s055", "page": "/pricing"} +{"ts": "2026-03-05T15:18:22Z", "session_id": "s024", "page": "/pricing"} +{"ts": "2026-03-05T15:19:58Z", "session_id": "s041", "page": "/docs"} +{"ts": "2026-03-05T15:20:39Z", "session_id": "s008", "page": "/"} +{"ts": "2026-03-05T15:22:01Z", "session_id": "s071", "page": "/blog"} +{"ts": "2026-03-05T15:22:42Z", "session_id": "s049", "page": "/pricing"} +{"ts": "2026-03-05T15:23:47Z", "session_id": "s049", "page": "/checkout"} +{"ts": "2026-03-05T15:25:47Z", "session_id": "s008", "page": "/blog"} +{"ts": "2026-03-05T15:26:14Z", "session_id": "s041", "page": "/checkout"} +{"ts": "2026-03-05T15:27:57Z", "session_id": "s035", "page": "/"} +{"ts": "2026-03-05T15:29:52Z", "session_id": "s030", "page": "/blog"} +{"ts": "2026-03-05T15:31:44Z", "session_id": "s020", "page": "/"} +{"ts": "2026-03-05T15:33:12Z", "session_id": "s066", "page": "/signup"} +{"ts": "2026-03-05T15:34:30Z", "session_id": "s052", "page": "/pricing"} +{"ts": "2026-03-05T15:36:40Z", "session_id": "s058", "page": "/"} +{"ts": "2026-03-05T15:37:42Z", "session_id": "s057", "page": "/blog"} +{"ts": "2026-03-05T15:38:42Z", "session_id": "s086", "page": "/"} +{"ts": "2026-03-05T15:39:11Z", "session_id": "s071", "page": "/signup"} +{"ts": "2026-03-05T15:40:22Z", "session_id": "s047", "page": "/checkout"} +{"ts": "2026-03-05T15:41:51Z", "session_id": "s081", "page": "/signup"} +{"ts": "2026-03-05T15:43:17Z", "session_id": "s002", "page": "/signup"} +{"ts": "2026-03-05T15:45:25Z", "session_id": "s084", "page": "/signup"} +{"ts": "2026-03-05T15:46:06Z", "session_id": "s032", "page": "/"} +{"ts": "2026-03-05T15:47:22Z", "session_id": "s043", "page": "/docs"} +{"ts": "2026-03-05T15:47:56Z", "session_id": "s004", "page": "/pricing"} +{"ts": "2026-03-05T15:49:49Z", "session_id": "s061", "page": "/"} +{"ts": "2026-03-05T15:50:24Z", "session_id": "s008", "page": "/blog"} +{"ts": "2026-03-05T15:51:02Z", "session_id": "s006", "page": "/checkout"} +{"ts": "2026-03-05T15:53:20Z", "session_id": "s031", "page": "/docs"} +{"ts": "2026-03-05T15:55:17Z", "session_id": "s062", "page": "/docs"} +{"ts": "2026-03-05T15:56:04Z", "session_id": "s016", "page": "/checkout"} +{"ts": "2026-03-05T15:58:15Z", "session_id": "s045", "page": "/checkout"} +{"ts": "2026-03-05T15:58:36Z", "session_id": "s087", "page": "/pricing"} +{"ts": "2026-03-05T16:00:29Z", "session_id": "s037", "page": "/pricing"} +{"ts": "2026-03-05T16:00:50Z", "session_id": "s081", "page": "/blog"} +{"ts": "2026-03-05T16:02:05Z", "session_id": "s004", "page": "/pricing"} +{"ts": "2026-03-05T16:03:49Z", "session_id": "s054", "page": "/blog"} +{"ts": "2026-03-05T16:05:55Z", "session_id": "s035", "page": "/checkout"} +{"ts": "2026-03-05T16:06:28Z", "session_id": "s062", "page": "/pricing"} +{"ts": "2026-03-05T16:08:17Z", "session_id": "s074", "page": "/docs"} +{"ts": "2026-03-05T16:09:45Z", "session_id": "s010", "page": "/checkout"} +{"ts": "2026-03-05T16:11:37Z", "session_id": "s054", "page": "/"} +{"ts": "2026-03-05T16:13:32Z", "session_id": "s071", "page": "/pricing"} +{"ts": "2026-03-05T16:14:57Z", "session_id": "s033", "page": "/blog"} +{"ts": "2026-03-05T16:15:24Z", "session_id": "s077", "page": "/blog"} +{"ts": "2026-03-05T16:17:13Z", "session_id": "s078", "page": "/signup"} +{"ts": "2026-03-05T16:18:23Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T16:19:06Z", "session_id": "s064", "page": "/docs"} +{"ts": "2026-03-05T16:19:29Z", "session_id": "s004", "page": "/"} +{"ts": "2026-03-05T16:21:01Z", "session_id": "s062", "page": "/signup"} +{"ts": "2026-03-05T16:23:18Z", "session_id": "s082", "page": "/docs"} +{"ts": "2026-03-05T16:25:30Z", "session_id": "s080", "page": "/checkout"} +{"ts": "2026-03-05T16:26:47Z", "session_id": "s012", "page": "/checkout"} +{"ts": "2026-03-05T16:28:08Z", "session_id": "s060", "page": "/blog"} +{"ts": "2026-03-05T16:28:39Z", "session_id": "s023", "page": "/checkout"} +{"ts": "2026-03-05T16:29:20Z", "session_id": "s054", "page": "/blog"} +{"ts": "2026-03-05T16:31:13Z", "session_id": "s031", "page": "/pricing"} +{"ts": "2026-03-05T16:33:15Z", "session_id": "s068", "page": "/pricing"} +{"ts": "2026-03-05T16:33:38Z", "session_id": "s055", "page": "/pricing"} +{"ts": "2026-03-05T16:34:29Z", "session_id": "s050", "page": "/pricing"} +{"ts": "2026-03-05T16:35:37Z", "session_id": "s017", "page": "/"} +{"ts": "2026-03-05T16:36:44Z", "session_id": "s043", "page": "/pricing"} +{"ts": "2026-03-05T16:37:17Z", "session_id": "s018", "page": "/pricing"} +{"ts": "2026-03-05T16:39:19Z", "session_id": "s060", "page": "/"} +{"ts": "2026-03-05T16:40:34Z", "session_id": "s003", "page": "/"} +{"ts": "2026-03-05T16:41:50Z", "session_id": "s062", "page": "/signup"} +{"ts": "2026-03-05T16:42:11Z", "session_id": "s064", "page": "/checkout"} +{"ts": "2026-03-05T16:43:30Z", "session_id": "s051", "page": "/docs"} +{"ts": "2026-03-05T16:43:57Z", "session_id": "s045", "page": "/pricing"} +{"ts": "2026-03-05T16:44:53Z", "session_id": "s080", "page": "/pricing"} +{"ts": "2026-03-05T16:45:50Z", "session_id": "s007", "page": "/blog"} +{"ts": "2026-03-05T16:47:28Z", "session_id": "s062", "page": "/"} +{"ts": "2026-03-05T16:49:19Z", "session_id": "s066", "page": "/"} +{"ts": "2026-03-05T16:51:35Z", "session_id": "s004", "page": "/"} +{"ts": "2026-03-05T16:52:22Z", "session_id": "s012", "page": "/pricing"} +{"ts": "2026-03-05T16:54:33Z", "session_id": "s045", "page": "/docs"} +{"ts": "2026-03-05T16:56:26Z", "session_id": "s063", "page": "/blog"} +{"ts": "2026-03-05T16:57:24Z", "session_id": "s023", "page": "/"} +{"ts": "2026-03-05T16:59:20Z", "session_id": "s071", "page": "/checkout"} +{"ts": "2026-03-05T17:01:04Z", "session_id": "s055", "page": "/"} +{"ts": "2026-03-05T17:03:07Z", "session_id": "s024", "page": "/"} +{"ts": "2026-03-05T17:04:09Z", "session_id": "s026", "page": "/"} +{"ts": "2026-03-05T17:06:27Z", "session_id": "s020", "page": "/pricing"} +{"ts": "2026-03-05T17:07:19Z", "session_id": "s070", "page": "/pricing"} +{"ts": "2026-03-05T17:09:20Z", "session_id": "s006", "page": "/pricing"} +{"ts": "2026-03-05T17:11:34Z", "session_id": "s023", "page": "/blog"} +{"ts": "2026-03-05T17:12:55Z", "session_id": "s075", "page": "/blog"} +{"ts": "2026-03-05T17:14:34Z", "session_id": "s037", "page": "/pricing"} +{"ts": "2026-03-05T17:16:25Z", "session_id": "s028", "page": "/signup"} +{"ts": "2026-03-05T17:18:04Z", "session_id": "s059", "page": "/pricing"} +{"ts": "2026-03-05T17:20:06Z", "session_id": "s038", "page": "/signup"} +{"ts": "2026-03-05T17:22:16Z", "session_id": "s057", "page": "/signup"} +{"ts": "2026-03-05T17:23:42Z", "session_id": "s054", "page": "/"} +{"ts": "2026-03-05T17:25:06Z", "session_id": "s085", "page": "/signup"} +{"ts": "2026-03-05T17:26:35Z", "session_id": "s032", "page": "/blog"} +{"ts": "2026-03-05T17:28:01Z", "session_id": "s028", "page": "/pricing"} +{"ts": "2026-03-05T17:30:08Z", "session_id": "s065", "page": "/"} +{"ts": "2026-03-05T17:30:55Z", "session_id": "s010", "page": "/docs"} +{"ts": "2026-03-05T17:32:43Z", "session_id": "s063", "page": "/signup"} +{"ts": "2026-03-05T17:33:29Z", "session_id": "s028", "page": "/checkout"} +{"ts": "2026-03-05T17:35:42Z", "session_id": "s071", "page": "/signup"} +{"ts": "2026-03-05T17:37:29Z", "session_id": "s078", "page": "/signup"} +{"ts": "2026-03-05T17:38:38Z", "session_id": "s062", "page": "/checkout"} +{"ts": "2026-03-05T17:39:24Z", "session_id": "s057", "page": "/docs"} +{"ts": "2026-03-05T17:41:31Z", "session_id": "s084", "page": "/pricing"} +{"ts": "2026-03-05T17:42:22Z", "session_id": "s019", "page": "/pricing"} +{"ts": "2026-03-05T17:43:53Z", "session_id": "s006", "page": "/docs"} +{"ts": "2026-03-05T17:44:51Z", "session_id": "s021", "page": "/checkout"} +{"ts": "2026-03-05T17:45:15Z", "session_id": "s043", "page": "/docs"} +{"ts": "2026-03-05T17:46:20Z", "session_id": "s089", "page": "/checkout"} +{"ts": "2026-03-05T17:46:40Z", "session_id": "s016", "page": "/signup"} +{"ts": "2026-03-05T17:48:51Z", "session_id": "s062", "page": "/checkout"} +{"ts": "2026-03-05T17:49:45Z", "session_id": "s017", "page": "/"} +{"ts": "2026-03-05T17:51:49Z", "session_id": "s034", "page": "/checkout"} +{"ts": "2026-03-05T17:54:02Z", "session_id": "s002", "page": "/checkout"} +{"ts": "2026-03-05T17:55:20Z", "session_id": "s083", "page": "/docs"} +{"ts": "2026-03-05T17:57:31Z", "session_id": "s045", "page": "/"} +{"ts": "2026-03-05T17:58:16Z", "session_id": "s040", "page": "/docs"} +{"ts": "2026-03-05T17:58:44Z", "session_id": "s044", "page": "/checkout"} +{"ts": "2026-03-05T18:00:36Z", "session_id": "s084", "page": "/checkout"} +{"ts": "2026-03-05T18:02:05Z", "session_id": "s011", "page": "/checkout"} +{"ts": "2026-03-05T18:03:09Z", "session_id": "s063", "page": "/"} +{"ts": "2026-03-05T18:04:43Z", "session_id": "s029", "page": "/blog"} +{"ts": "2026-03-05T18:06:03Z", "session_id": "s016", "page": "/checkout"} +{"ts": "2026-03-05T18:06:53Z", "session_id": "s046", "page": "/docs"} +{"ts": "2026-03-05T18:08:51Z", "session_id": "s012", "page": "/signup"} +{"ts": "2026-03-05T18:10:19Z", "session_id": "s043", "page": "/docs"} +{"ts": "2026-03-05T18:11:55Z", "session_id": "s029", "page": "/blog"} +{"ts": "2026-03-05T18:13:51Z", "session_id": "s032", "page": "/checkout"} +{"ts": "2026-03-05T18:14:14Z", "session_id": "s056", "page": "/signup"} +{"ts": "2026-03-05T18:14:49Z", "session_id": "s011", "page": "/checkout"} +{"ts": "2026-03-05T18:16:23Z", "session_id": "s062", "page": "/signup"} +{"ts": "2026-03-05T18:17:57Z", "session_id": "s036", "page": "/"} +{"ts": "2026-03-05T18:19:30Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T18:20:00Z", "session_id": "s016", "page": "/signup"} +{"ts": "2026-03-05T18:20:46Z", "session_id": "s080", "page": "/checkout"} +{"ts": "2026-03-05T18:22:07Z", "session_id": "s055", "page": "/checkout"} +{"ts": "2026-03-05T18:23:03Z", "session_id": "s043", "page": "/blog"} +{"ts": "2026-03-05T18:23:34Z", "session_id": "s058", "page": "/"} +{"ts": "2026-03-05T18:24:25Z", "session_id": "s031", "page": "/pricing"} +{"ts": "2026-03-05T18:26:27Z", "session_id": "s051", "page": "/pricing"} +{"ts": "2026-03-05T18:28:33Z", "session_id": "s003", "page": "/pricing"} +{"ts": "2026-03-05T18:30:08Z", "session_id": "s072", "page": "/docs"} +{"ts": "2026-03-05T18:32:20Z", "session_id": "s017", "page": "/signup"} +{"ts": "2026-03-05T18:33:46Z", "session_id": "s040", "page": "/blog"} +{"ts": "2026-03-05T18:34:34Z", "session_id": "s019", "page": "/blog"} +{"ts": "2026-03-05T18:35:20Z", "session_id": "s054", "page": "/blog"} +{"ts": "2026-03-05T18:36:15Z", "session_id": "s077", "page": "/"} +{"ts": "2026-03-05T18:36:48Z", "session_id": "s049", "page": "/pricing"} +{"ts": "2026-03-05T18:38:17Z", "session_id": "s030", "page": "/pricing"} +{"ts": "2026-03-05T18:39:11Z", "session_id": "s048", "page": "/checkout"} +{"ts": "2026-03-05T18:40:52Z", "session_id": "s041", "page": "/signup"} +{"ts": "2026-03-05T18:42:20Z", "session_id": "s039", "page": "/pricing"} +{"ts": "2026-03-05T18:44:12Z", "session_id": "s025", "page": "/blog"} +{"ts": "2026-03-05T18:45:02Z", "session_id": "s019", "page": "/"} +{"ts": "2026-03-05T18:45:44Z", "session_id": "s036", "page": "/pricing"} +{"ts": "2026-03-05T18:47:01Z", "session_id": "s021", "page": "/pricing"} +{"ts": "2026-03-05T18:47:56Z", "session_id": "s050", "page": "/signup"} +{"ts": "2026-03-05T18:49:35Z", "session_id": "s072", "page": "/signup"} +{"ts": "2026-03-05T18:51:21Z", "session_id": "s028", "page": "/pricing"} +{"ts": "2026-03-05T18:52:01Z", "session_id": "s082", "page": "/checkout"} +{"ts": "2026-03-05T18:52:30Z", "session_id": "s025", "page": "/pricing"} +{"ts": "2026-03-05T18:53:02Z", "session_id": "s054", "page": "/docs"} +{"ts": "2026-03-05T18:53:37Z", "session_id": "s086", "page": "/blog"} +{"ts": "2026-03-05T18:54:47Z", "session_id": "s064", "page": "/pricing"} +{"ts": "2026-03-05T18:56:45Z", "session_id": "s086", "page": "/signup"} +{"ts": "2026-03-05T18:57:43Z", "session_id": "s051", "page": "/"} +{"ts": "2026-03-05T18:59:59Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T19:00:43Z", "session_id": "s076", "page": "/signup"} +{"ts": "2026-03-05T19:01:53Z", "session_id": "s022", "page": "/checkout"} +{"ts": "2026-03-05T19:02:17Z", "session_id": "s014", "page": "/"} +{"ts": "2026-03-05T19:03:55Z", "session_id": "s023", "page": "/checkout"} +{"ts": "2026-03-05T19:04:19Z", "session_id": "s055", "page": "/blog"} +{"ts": "2026-03-05T19:05:03Z", "session_id": "s032", "page": "/checkout"} +{"ts": "2026-03-05T19:06:01Z", "session_id": "s071", "page": "/signup"} +{"ts": "2026-03-05T19:07:03Z", "session_id": "s013", "page": "/signup"} +{"ts": "2026-03-05T19:08:07Z", "session_id": "s066", "page": "/signup"} +{"ts": "2026-03-05T19:09:47Z", "session_id": "s036", "page": "/"} +{"ts": "2026-03-05T19:11:06Z", "session_id": "s086", "page": "/pricing"} +{"ts": "2026-03-05T19:12:53Z", "session_id": "s043", "page": "/pricing"} +{"ts": "2026-03-05T19:13:50Z", "session_id": "s082", "page": "/blog"} +{"ts": "2026-03-05T19:14:13Z", "session_id": "s020", "page": "/docs"} +{"ts": "2026-03-05T19:15:17Z", "session_id": "s001", "page": "/pricing"} +{"ts": "2026-03-05T19:15:54Z", "session_id": "s012", "page": "/pricing"} +{"ts": "2026-03-05T19:17:14Z", "session_id": "s088", "page": "/signup"} +{"ts": "2026-03-05T19:18:31Z", "session_id": "s033", "page": "/blog"} +{"ts": "2026-03-05T19:19:47Z", "session_id": "s051", "page": "/pricing"} +{"ts": "2026-03-05T19:21:33Z", "session_id": "s006", "page": "/"} +{"ts": "2026-03-05T19:23:19Z", "session_id": "s080", "page": "/checkout"} +{"ts": "2026-03-05T19:24:41Z", "session_id": "s020", "page": "/signup"} +{"ts": "2026-03-05T19:26:20Z", "session_id": "s075", "page": "/checkout"} +{"ts": "2026-03-05T19:26:43Z", "session_id": "s066", "page": "/pricing"} +{"ts": "2026-03-05T19:28:02Z", "session_id": "s004", "page": "/checkout"} +{"ts": "2026-03-05T19:29:41Z", "session_id": "s030", "page": "/checkout"} +{"ts": "2026-03-05T19:31:12Z", "session_id": "s084", "page": "/pricing"} +{"ts": "2026-03-05T19:33:17Z", "session_id": "s065", "page": "/blog"} +{"ts": "2026-03-05T19:35:27Z", "session_id": "s068", "page": "/checkout"} +{"ts": "2026-03-05T19:36:01Z", "session_id": "s029", "page": "/pricing"} +{"ts": "2026-03-05T19:36:51Z", "session_id": "s020", "page": "/pricing"} +{"ts": "2026-03-05T19:38:01Z", "session_id": "s074", "page": "/checkout"} +{"ts": "2026-03-05T19:40:07Z", "session_id": "s068", "page": "/checkout"} +{"ts": "2026-03-05T19:41:25Z", "session_id": "s048", "page": "/pricing"} +{"ts": "2026-03-05T19:42:09Z", "session_id": "s014", "page": "/"} +{"ts": "2026-03-05T19:44:18Z", "session_id": "s051", "page": "/blog"} +{"ts": "2026-03-05T19:45:12Z", "session_id": "s022", "page": "/"} +{"ts": "2026-03-05T19:46:59Z", "session_id": "s053", "page": "/blog"} +{"ts": "2026-03-05T19:49:13Z", "session_id": "s033", "page": "/pricing"} +{"ts": "2026-03-05T19:51:07Z", "session_id": "s083", "page": "/"} +{"ts": "2026-03-05T19:52:59Z", "session_id": "s006", "page": "/"} +{"ts": "2026-03-05T19:53:41Z", "session_id": "s014", "page": "/docs"} +{"ts": "2026-03-05T19:54:01Z", "session_id": "s060", "page": "/blog"} +{"ts": "2026-03-05T19:55:26Z", "session_id": "s088", "page": "/checkout"} +{"ts": "2026-03-05T19:57:06Z", "session_id": "s017", "page": "/pricing"} +{"ts": "2026-03-05T19:58:51Z", "session_id": "s073", "page": "/pricing"} +{"ts": "2026-03-05T20:01:04Z", "session_id": "s042", "page": "/checkout"} +{"ts": "2026-03-05T20:03:07Z", "session_id": "s019", "page": "/signup"} +{"ts": "2026-03-05T20:04:43Z", "session_id": "s011", "page": "/pricing"} +{"ts": "2026-03-05T20:06:44Z", "session_id": "s043", "page": "/blog"} +{"ts": "2026-03-05T20:08:52Z", "session_id": "s054", "page": "/pricing"} +{"ts": "2026-03-05T20:09:20Z", "session_id": "s015", "page": "/pricing"} +{"ts": "2026-03-05T20:11:18Z", "session_id": "s056", "page": "/checkout"} +{"ts": "2026-03-05T20:13:12Z", "session_id": "s031", "page": "/signup"} +{"ts": "2026-03-05T20:13:56Z", "session_id": "s018", "page": "/blog"} +{"ts": "2026-03-05T20:14:29Z", "session_id": "s034", "page": "/pricing"} +{"ts": "2026-03-05T20:15:12Z", "session_id": "s062", "page": "/docs"} +{"ts": "2026-03-05T20:16:00Z", "session_id": "s001", "page": "/docs"} +{"ts": "2026-03-05T20:16:52Z", "session_id": "s002", "page": "/pricing"} +{"ts": "2026-03-05T20:18:40Z", "session_id": "s064", "page": "/"} +{"ts": "2026-03-05T20:20:07Z", "session_id": "s014", "page": "/checkout"} +{"ts": "2026-03-05T20:21:31Z", "session_id": "s015", "page": "/"} +{"ts": "2026-03-05T20:22:39Z", "session_id": "s044", "page": "/signup"} +{"ts": "2026-03-05T20:23:02Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T20:23:52Z", "session_id": "s079", "page": "/docs"} +{"ts": "2026-03-05T20:24:20Z", "session_id": "s031", "page": "/blog"} +{"ts": "2026-03-05T20:24:45Z", "session_id": "s056", "page": "/blog"} +{"ts": "2026-03-05T20:26:30Z", "session_id": "s041", "page": "/"} +{"ts": "2026-03-05T20:27:46Z", "session_id": "s031", "page": "/signup"} +{"ts": "2026-03-05T20:28:52Z", "session_id": "s029", "page": "/signup"} +{"ts": "2026-03-05T20:30:07Z", "session_id": "s069", "page": "/signup"} +{"ts": "2026-03-05T20:31:49Z", "session_id": "s033", "page": "/docs"} +{"ts": "2026-03-05T20:32:16Z", "session_id": "s042", "page": "/blog"} +{"ts": "2026-03-05T20:33:02Z", "session_id": "s020", "page": "/signup"} +{"ts": "2026-03-05T20:33:40Z", "session_id": "s039", "page": "/blog"} +{"ts": "2026-03-05T20:34:13Z", "session_id": "s079", "page": "/signup"} +{"ts": "2026-03-05T20:34:36Z", "session_id": "s032", "page": "/blog"} +{"ts": "2026-03-05T20:35:13Z", "session_id": "s036", "page": "/blog"} +{"ts": "2026-03-05T20:36:12Z", "session_id": "s071", "page": "/docs"} +{"ts": "2026-03-05T20:37:45Z", "session_id": "s033", "page": "/signup"} +{"ts": "2026-03-05T20:39:26Z", "session_id": "s077", "page": "/"} +{"ts": "2026-03-05T20:40:57Z", "session_id": "s005", "page": "/pricing"} +{"ts": "2026-03-05T20:41:58Z", "session_id": "s044", "page": "/signup"} +{"ts": "2026-03-05T20:42:18Z", "session_id": "s001", "page": "/checkout"} +{"ts": "2026-03-05T20:44:35Z", "session_id": "s080", "page": "/signup"} +{"ts": "2026-03-05T20:45:54Z", "session_id": "s054", "page": "/docs"} +{"ts": "2026-03-05T20:46:22Z", "session_id": "s069", "page": "/signup"} +{"ts": "2026-03-05T20:47:41Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T20:48:26Z", "session_id": "s014", "page": "/pricing"} +{"ts": "2026-03-05T20:48:52Z", "session_id": "s050", "page": "/docs"} +{"ts": "2026-03-05T20:49:43Z", "session_id": "s058", "page": "/checkout"} +{"ts": "2026-03-05T20:51:45Z", "session_id": "s049", "page": "/docs"} +{"ts": "2026-03-05T20:53:34Z", "session_id": "s008", "page": "/signup"} +{"ts": "2026-03-05T20:55:08Z", "session_id": "s051", "page": "/pricing"} +{"ts": "2026-03-05T20:56:40Z", "session_id": "s024", "page": "/docs"} +{"ts": "2026-03-05T20:57:19Z", "session_id": "s034", "page": "/pricing"} +{"ts": "2026-03-05T20:58:38Z", "session_id": "s056", "page": "/"} +{"ts": "2026-03-05T20:59:16Z", "session_id": "s076", "page": "/docs"} +{"ts": "2026-03-05T21:00:45Z", "session_id": "s006", "page": "/pricing"} +{"ts": "2026-03-05T21:02:33Z", "session_id": "s048", "page": "/"} +{"ts": "2026-03-05T21:04:21Z", "session_id": "s085", "page": "/checkout"} +{"ts": "2026-03-05T21:05:05Z", "session_id": "s072", "page": "/checkout"} +{"ts": "2026-03-05T21:06:07Z", "session_id": "s078", "page": "/pricing"} +{"ts": "2026-03-05T21:07:42Z", "session_id": "s006", "page": "/"} +{"ts": "2026-03-05T21:09:37Z", "session_id": "s047", "page": "/signup"} +{"ts": "2026-03-05T21:11:00Z", "session_id": "s085", "page": "/docs"} +{"ts": "2026-03-05T21:13:07Z", "session_id": "s012", "page": "/signup"} +{"ts": "2026-03-05T21:13:52Z", "session_id": "s062", "page": "/blog"} +{"ts": "2026-03-05T21:15:34Z", "session_id": "s055", "page": "/signup"} +{"ts": "2026-03-05T21:16:16Z", "session_id": "s087", "page": "/"} +{"ts": "2026-03-05T21:18:21Z", "session_id": "s046", "page": "/checkout"} +{"ts": "2026-03-05T21:20:27Z", "session_id": "s085", "page": "/"} +{"ts": "2026-03-05T21:22:15Z", "session_id": "s004", "page": "/pricing"} +{"ts": "2026-03-05T21:23:52Z", "session_id": "s016", "page": "/docs"} +{"ts": "2026-03-05T21:25:02Z", "session_id": "s052", "page": "/signup"} +{"ts": "2026-03-05T21:27:21Z", "session_id": "s016", "page": "/pricing"} +{"ts": "2026-03-05T21:28:22Z", "session_id": "s074", "page": "/checkout"} +{"ts": "2026-03-05T21:29:24Z", "session_id": "s007", "page": "/docs"} +{"ts": "2026-03-05T21:30:52Z", "session_id": "s033", "page": "/signup"} +{"ts": "2026-03-05T21:32:18Z", "session_id": "s039", "page": "/checkout"} +{"ts": "2026-03-05T21:33:58Z", "session_id": "s027", "page": "/docs"} +{"ts": "2026-03-05T21:35:47Z", "session_id": "s039", "page": "/checkout"} +{"ts": "2026-03-05T21:37:52Z", "session_id": "s066", "page": "/docs"} +{"ts": "2026-03-05T21:40:03Z", "session_id": "s084", "page": "/pricing"} +{"ts": "2026-03-05T21:42:03Z", "session_id": "s003", "page": "/checkout"} +{"ts": "2026-03-05T21:43:42Z", "session_id": "s030", "page": "/docs"} +{"ts": "2026-03-05T21:44:05Z", "session_id": "s050", "page": "/pricing"} +{"ts": "2026-03-05T21:44:54Z", "session_id": "s079", "page": "/docs"} +{"ts": "2026-03-05T21:46:07Z", "session_id": "s055", "page": "/signup"} +{"ts": "2026-03-05T21:47:23Z", "session_id": "s046", "page": "/"} +{"ts": "2026-03-05T21:48:32Z", "session_id": "s050", "page": "/checkout"} +{"ts": "2026-03-05T21:49:33Z", "session_id": "s065", "page": "/checkout"} +{"ts": "2026-03-05T21:50:19Z", "session_id": "s037", "page": "/pricing"} +{"ts": "2026-03-05T21:51:18Z", "session_id": "s012", "page": "/checkout"} +{"ts": "2026-03-05T21:52:03Z", "session_id": "s051", "page": "/checkout"} +{"ts": "2026-03-05T21:54:04Z", "session_id": "s046", "page": "/"} +{"ts": "2026-03-05T21:55:55Z", "session_id": "s067", "page": "/"} +{"ts": "2026-03-05T21:58:00Z", "session_id": "s026", "page": "/blog"} +{"ts": "2026-03-05T21:58:58Z", "session_id": "s012", "page": "/checkout"} +{"ts": "2026-03-05T21:59:40Z", "session_id": "s078", "page": "/"} +{"ts": "2026-03-05T22:01:15Z", "session_id": "s001", "page": "/checkout"} +{"ts": "2026-03-05T22:02:57Z", "session_id": "s003", "page": "/blog"} +{"ts": "2026-03-05T22:04:22Z", "session_id": "s083", "page": "/signup"} +{"ts": "2026-03-05T22:04:59Z", "session_id": "s021", "page": "/checkout"} +{"ts": "2026-03-05T22:05:28Z", "session_id": "s056", "page": "/signup"} +{"ts": "2026-03-05T22:07:24Z", "session_id": "s001", "page": "/"} +{"ts": "2026-03-05T22:07:46Z", "session_id": "s001", "page": "/blog"} +{"ts": "2026-03-05T22:09:20Z", "session_id": "s019", "page": "/docs"} +{"ts": "2026-03-05T22:10:20Z", "session_id": "s012", "page": "/docs"} +{"ts": "2026-03-05T22:11:29Z", "session_id": "s036", "page": "/checkout"} +{"ts": "2026-03-05T22:12:36Z", "session_id": "s063", "page": "/docs"} +{"ts": "2026-03-05T22:13:57Z", "session_id": "s079", "page": "/"} +{"ts": "2026-03-05T22:14:40Z", "session_id": "s050", "page": "/pricing"} +{"ts": "2026-03-05T22:16:17Z", "session_id": "s020", "page": "/docs"} +{"ts": "2026-03-05T22:17:54Z", "session_id": "s070", "page": "/blog"} +{"ts": "2026-03-05T22:19:12Z", "session_id": "s012", "page": "/"} +{"ts": "2026-03-05T22:20:12Z", "session_id": "s018", "page": "/blog"} +{"ts": "2026-03-05T22:20:53Z", "session_id": "s047", "page": "/"} +{"ts": "2026-03-05T22:22:32Z", "session_id": "s089", "page": "/checkout"} +{"ts": "2026-03-05T22:24:01Z", "session_id": "s068", "page": "/blog"} +{"ts": "2026-03-05T22:25:35Z", "session_id": "s088", "page": "/signup"} +{"ts": "2026-03-05T22:27:09Z", "session_id": "s056", "page": "/signup"} +{"ts": "2026-03-05T22:27:47Z", "session_id": "s077", "page": "/pricing"} +{"ts": "2026-03-05T22:29:29Z", "session_id": "s046", "page": "/pricing"} +{"ts": "2026-03-05T22:31:39Z", "session_id": "s052", "page": "/"} +{"ts": "2026-03-05T22:32:58Z", "session_id": "s039", "page": "/blog"} +{"ts": "2026-03-05T22:33:40Z", "session_id": "s021", "page": "/pricing"} +{"ts": "2026-03-05T22:35:33Z", "session_id": "s085", "page": "/docs"} +{"ts": "2026-03-05T22:37:44Z", "session_id": "s032", "page": "/"} +{"ts": "2026-03-05T22:38:22Z", "session_id": "s079", "page": "/"} +{"ts": "2026-03-05T22:39:40Z", "session_id": "s008", "page": "/signup"} +{"ts": "2026-03-05T22:40:50Z", "session_id": "s023", "page": "/"} +{"ts": "2026-03-05T22:42:46Z", "session_id": "s032", "page": "/pricing"} +{"ts": "2026-03-05T22:44:59Z", "session_id": "s025", "page": "/docs"} +{"ts": "2026-03-05T22:46:19Z", "session_id": "s020", "page": "/docs"} +{"ts": "2026-03-05T22:47:00Z", "session_id": "s047", "page": "/checkout"} +{"ts": "2026-03-05T22:49:07Z", "session_id": "s084", "page": "/"} +{"ts": "2026-03-05T22:50:55Z", "session_id": "s013", "page": "/checkout"} +{"ts": "2026-03-05T22:51:20Z", "session_id": "s084", "page": "/checkout"} +{"ts": "2026-03-05T22:51:50Z", "session_id": "s067", "page": "/blog"} +{"ts": "2026-03-05T22:53:57Z", "session_id": "s055", "page": "/checkout"} +{"ts": "2026-03-05T22:56:15Z", "session_id": "s059", "page": "/blog"} +{"ts": "2026-03-05T22:57:34Z", "session_id": "s006", "page": "/pricing"} +{"ts": "2026-03-05T22:59:46Z", "session_id": "s012", "page": "/blog"} +{"ts": "2026-03-05T23:00:00Z", "session_id": "s999", "page": "/"} +{"ts": "2026-03-05T23:01:49Z", "session_id": "s085", "page": "/checkout"} +{"ts": "2026-03-05T23:03:19Z", "session_id": "s073", "page": "/checkout"} +{"ts": "2026-03-05T23:04:55Z", "session_id": "s046", "page": "/signup"} +{"ts": "2026-03-05T23:05:31Z", "session_id": "s017", "page": "/pricing"} +{"ts": "2026-03-05T23:06:00Z", "session_id": "s089", "page": "/blog"} +{"ts": "2026-03-05T23:08:16Z", "session_id": "s079", "page": "/blog"} +{"ts": "2026-03-05T23:08:48Z", "session_id": "s089", "page": "/docs"} +{"ts": "2026-03-05T23:10:29Z", "session_id": "s044", "page": "/signup"} +{"ts": "2026-03-05T23:12:12Z", "session_id": "s024", "page": "/checkout"} +{"ts": "2026-03-05T23:13:39Z", "session_id": "s080", "page": "/checkout"} +{"ts": "2026-03-05T23:15:28Z", "session_id": "s062", "page": "/checkout"} +{"ts": "2026-03-05T23:17:08Z", "session_id": "s077", "page": "/checkout"} +{"ts": "2026-03-05T23:18:26Z", "session_id": "s013", "page": "/checkout"} +{"ts": "2026-03-05T23:19:50Z", "session_id": "s053", "page": "/"} +{"ts": "2026-03-05T23:21:18Z", "session_id": "s077", "page": "/checkout"} +{"ts": "2026-03-05T23:21:58Z", "session_id": "s031", "page": "/"} +{"ts": "2026-03-05T23:24:08Z", "session_id": "s015", "page": "/docs"} +{"ts": "2026-03-05T23:25:43Z", "session_id": "s047", "page": "/blog"} +{"ts": "2026-03-05T23:27:00Z", "session_id": "s001", "page": "/pricing"} +{"ts": "2026-03-05T23:29:14Z", "session_id": "s071", "page": "/"} +{"ts": "2026-03-05T23:29:59Z", "session_id": "s057", "page": "/signup"} +{"ts": "2026-03-05T23:31:52Z", "session_id": "s070", "page": "/checkout"} +{"ts": "2026-03-05T23:34:07Z", "session_id": "s014", "page": "/docs"} +{"ts": "2026-03-05T23:35:35Z", "session_id": "s009", "page": "/"} +{"ts": "2026-03-05T23:36:32Z", "session_id": "s073", "page": "/pricing"} +{"ts": "2026-03-05T23:37:30Z", "session_id": "s057", "page": "/"} +{"ts": "2026-03-05T23:39:35Z", "session_id": "s053", "page": "/checkout"} +{"ts": "2026-03-05T23:41:02Z", "session_id": "s078", "page": "/"} +{"ts": "2026-03-05T23:41:46Z", "session_id": "s022", "page": "/"} +{"ts": "2026-03-05T23:43:53Z", "session_id": "s038", "page": "/docs"} +{"ts": "2026-03-05T23:44:54Z", "session_id": "s025", "page": "/signup"} +{"ts": "2026-03-05T23:45:36Z", "session_id": "s047", "page": "/pricing"} +{"ts": "2026-03-05T23:47:26Z", "session_id": "s069", "page": "/"} +{"ts": "2026-03-05T23:48:29Z", "session_id": "s020", "page": "/blog"} +{"ts": "2026-03-05T23:49:41Z", "session_id": "s002", "page": "/signup"} +{"ts": "2026-03-05T23:50:55Z", "session_id": "s081", "page": "/blog"} +{"ts": "2026-03-05T23:53:06Z", "session_id": "s031", "page": "/blog"} +{"ts": "2026-03-05T23:55:00Z", "session_id": "s073", "page": "/pricing"} +{"ts": "2026-03-05T23:55:48Z", "session_id": "s004", "page": "/"} +{"ts": "2026-03-05T23:56:39Z", "session_id": "s004", "page": "/docs"} +{"ts": "2026-03-05T23:57:03Z", "session_id": "s079", "page": "/signup"} +{"ts": "2026-03-05T23:58:53Z", "session_id": "s087", "page": "/signup"} +{"ts": "2026-03-05T23:59:38Z", "session_id": "s078", "page": "/checkout"} +{"ts": "2026-03-06T00:00:00Z", "session_id": "s999", "page": "/"} +{"ts": "2026-03-06T00:01:32Z", "session_id": "s043", "page": "/checkout"} +{"ts": "2026-03-06T00:03:21Z", "session_id": "s022", "page": "/signup"} +{"ts": "2026-03-06T00:05:12Z", "session_id": "s085", "page": "/checkout"} +{"ts": "2026-03-06T00:05:53Z", "session_id": "s048", "page": "/blog"} +{"ts": "2026-03-06T00:08:00Z", "session_id": "s017", "page": "/blog"} +{"ts": "2026-03-06T00:10:08Z", "session_id": "s057", "page": "/signup"} +{"ts": "2026-03-06T00:10:48Z", "session_id": "s067", "page": "/checkout"} +{"ts": "2026-03-06T00:12:15Z", "session_id": "s040", "page": "/checkout"} +{"ts": "2026-03-06T00:13:17Z", "session_id": "s054", "page": "/"} +{"ts": "2026-03-06T00:13:44Z", "session_id": "s021", "page": "/blog"} +{"ts": "2026-03-06T00:14:43Z", "session_id": "s068", "page": "/docs"} +{"ts": "2026-03-06T00:15:55Z", "session_id": "s009", "page": "/pricing"} +{"ts": "2026-03-06T00:17:32Z", "session_id": "s061", "page": "/"} +{"ts": "2026-03-06T00:19:04Z", "session_id": "s026", "page": "/docs"} +{"ts": "2026-03-06T00:19:40Z", "session_id": "s003", "page": "/pricing"} +{"ts": "2026-03-06T00:20:24Z", "session_id": "s086", "page": "/signup"} +{"ts": "2026-03-06T00:22:20Z", "session_id": "s035", "page": "/checkout"} +{"ts": "2026-03-06T00:24:11Z", "session_id": "s068", "page": "/pricing"} +{"ts": "2026-03-06T00:26:03Z", "session_id": "s065", "page": "/checkout"} +{"ts": "2026-03-06T00:26:47Z", "session_id": "s065", "page": "/docs"} +{"ts": "2026-03-06T00:27:26Z", "session_id": "s062", "page": "/docs"} +{"ts": "2026-03-06T00:28:55Z", "session_id": "s041", "page": "/signup"} +{"ts": "2026-03-06T00:29:55Z", "session_id": "s079", "page": "/signup"} +{"ts": "2026-03-06T00:31:50Z", "session_id": "s033", "page": "/docs"} +{"ts": "2026-03-06T00:32:19Z", "session_id": "s001", "page": "/checkout"} +{"ts": "2026-03-06T00:32:44Z", "session_id": "s045", "page": "/checkout"} +{"ts": "2026-03-06T00:33:52Z", "session_id": "s054", "page": "/"} +{"ts": "2026-03-06T00:36:05Z", "session_id": "s076", "page": "/signup"} +{"ts": "2026-03-06T00:37:32Z", "session_id": "s044", "page": "/blog"} +{"ts": "2026-03-06T00:38:52Z", "session_id": "s063", "page": "/signup"} +{"ts": "2026-03-06T00:39:47Z", "session_id": "s040", "page": "/pricing"} +{"ts": "2026-03-06T00:40:58Z", "session_id": "s079", "page": "/pricing"} +{"ts": "2026-03-06T00:42:42Z", "session_id": "s038", "page": "/docs"} +{"ts": "2026-03-06T00:43:55Z", "session_id": "s063", "page": "/"} +{"ts": "2026-03-06T00:44:52Z", "session_id": "s023", "page": "/"} +{"ts": "2026-03-06T00:45:29Z", "session_id": "s048", "page": "/signup"} +{"ts": "2026-03-06T00:46:41Z", "session_id": "s029", "page": "/checkout"} +{"ts": "2026-03-06T00:48:48Z", "session_id": "s089", "page": "/docs"} +{"ts": "2026-03-06T00:50:20Z", "session_id": "s016", "page": "/docs"} +{"ts": "2026-03-06T00:51:01Z", "session_id": "s051", "page": "/docs"} +{"ts": "2026-03-06T00:52:49Z", "session_id": "s005", "page": "/"} +{"ts": "2026-03-06T00:54:01Z", "session_id": "s088", "page": "/signup"} +{"ts": "2026-03-06T00:55:50Z", "session_id": "s063", "page": "/"} +{"ts": "2026-03-06T00:57:27Z", "session_id": "s009", "page": "/signup"} +{"ts": "2026-03-06T00:58:55Z", "session_id": "s026", "page": "/signup"} +{"ts": "2026-03-06T01:00:00Z", "session_id": "s050", "page": "/pricing"} diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/requirements.txt b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/requirements.txt new file mode 100644 index 0000000..7056ae9 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==2.11.2 +pendulum==3.2.0 diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/grade.py b/evals/airflow/cases/migration-hourly-pageviews-templates/grade.py new file mode 100644 index 0000000..30314ad --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/grade.py @@ -0,0 +1,104 @@ +"""Grader for migration-hourly-pageviews-templates. + +The fixture is an Airflow 2.11 hourly rollup whose templated fields use context +variables Airflow 3 removed: ``{{ execution_date }}``, ``{{ next_execution_date }}``, +``{{ prev_execution_date }}`` (PythonOperator ``op_kwargs``) and +``{{ yesterday_ds_nodash }}`` (BashOperator cleanup). Ruff's AIR rules do not +look inside Jinja strings. The bare cron "0 * * * *" also becomes +CronTriggerTimetable in Airflow 3, so the 10:00 run would no longer process +09:00-10:00. + +Behaviour is compared with the ORIGINAL project replayed in the 2.11 env: +three scheduled runs (10:00 and 11:00 on 2026-03-05 and the midnight run on +2026-03-06) against one DB, starting from the same pre-seeded staging area. +The midnight run checks that the staging cleanup still targets the day before +the processed hour. +""" + +from __future__ import annotations + +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import migsim # noqa: E402 + +g = migsim.g +DAG_ID = "hourly_pageviews" +RUN_AFTERS = ["2026-03-05T10:00:00+00:00", "2026-03-05T11:00:00+00:00", "2026-03-06T00:00:00+00:00"] +EXPECTED_TASKS = {"extract_hour", "aggregate_hour", "publish_hour"} +EXPECTED_DEPS = [["aggregate_hour", "publish_hour"], ["extract_hour", "aggregate_hour"]] +SEED_STAGING = { + "20260304T22.csv": "ts,session_id,page\n2026-03-04T22:15:00Z,s001,/\n", + "20260304T23.csv": "ts,session_id,page\n2026-03-04T23:40:00Z,s002,/docs\n", + "20260305T08.csv": "ts,session_id,page\n2026-03-05T08:05:00Z,s003,/pricing\n", +} + + +def seed(ws: Path) -> None: + shutil.rmtree(ws / "output", ignore_errors=True) + staging = ws / "output" / "staging" + staging.mkdir(parents=True) + for name, text in SEED_STAGING.items(): + (staging / name).write_text(text) + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + orig_ws, orig, orig_log = migsim.run_original(CASE_DIR, DAG_ID, RUN_AFTERS, prepare=seed) + if not migsim.runs_ok(orig): + raise SystemExit(f"case bug: original project failed in 2.11: {migsim.describe(orig)}\n{orig_log}") + expected = migsim.snapshot(orig_ws) + if "staging/20260304T22.csv" in expected or not any(p.startswith("markers/") for p in expected): + raise SystemExit(f"case bug: unexpected original output {sorted(expected)}") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAG imports cleanly on Airflow 3.3", imp["ok"] and not imp["import_errors"] and dag is not None, + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"dags: {sorted(imp['dags'])}") + grader.primary("hourly_pageviews keeps its tasks and dependencies", + dag is not None and set(dag["tasks"]) == EXPECTED_TASKS + and {tuple(d) for d in EXPECTED_DEPS} <= {tuple(d) for d in dag["deps"]}, + f"tasks={dag and dag['tasks']} deps={dag and dag['deps']}") + + if dag is None: + grader.primary("dags test succeeds", False, "DAG missing") + else: + seed(ws) + cli, cli_log = migsim.simulate(py, ws, DAG_ID, [migsim.manual("2026-03-05T10:00:00+00:00")], block_orm=True) + grader.primary("dags test succeeds", migsim.runs_ok(cli), + migsim.describe(cli) + ("" if migsim.runs_ok(cli) else "\n" + cli_log[-2500:])) + + seed(ws) + sim, sim_log = migsim.simulate(py, ws, DAG_ID, RUN_AFTERS, block_orm=True) + grader.primary("scheduled hourly runs succeed on 3.3", migsim.runs_ok(sim), + migsim.describe(sim) + ("" if migsim.runs_ok(sim) else "\n" + sim_log[-2500:])) + got = migsim.snapshot(ws) + + def part(snap: dict[str, str], prefix: str) -> dict[str, str]: + return {k: v for k, v in snap.items() if k.startswith(prefix)} + + for prefix, label in (("hourly/", "hourly rollups (previous clock hour, change vs hour before) match 2.11"), + ("markers/", "completion markers match 2.11"), + ("staging/", "staging cleanup keeps and deletes the same files as 2.11")): + exp_part, got_part = part(expected, prefix), part(got, prefix) + if prefix == "staging/": + detail = f"expected files {sorted(exp_part)}\n got {sorted(got_part)}" + else: + detail = f"expected {exp_part}\n got {got_part}" + grader.primary(label, got_part == exp_part, detail) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/migsim.py b/evals/airflow/cases/migration-hourly-pageviews-templates/migsim.py new file mode 100644 index 0000000..ffa3509 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/migsim.py @@ -0,0 +1,260 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +This file is identical in every ``migration-*`` case directory. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + dag = load_dag(P["dag_id"]) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + tt = core_timetable(dag) + RESULT["timetable"] = type(tt).__name__ + for spec in P["runs"]: + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(P["dag_id"], logical) + forgive_deadline_bug(row) + continue + run_after = spec + row = {"run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {P['dag_id']} fires at {run_after} ({type(tt).__name__})" + continue + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(P["dag_id"], info.logical_date) + forgive_deadline_bug(row) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is either an ISO datetime T (the *scheduled* run that fires at T, + with the logical date and data interval the DAG's timetable gives it) or + ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), "dag_id": dag_id, + "runs": list(runs), "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + return "; ".join( + f"{r.get('run_after') or 'manual ' + str(r.get('manual'))}: " + (r.get("error") or f"state={r.get('state')} interval=[{r.get('start')}, {r.get('end')}]" + f" failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/cleanup_relative_to_run_time/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/cleanup_relative_to_run_time/dags/hourly_pageviews.py new file mode 100644 index 0000000..5a0ae08 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/cleanup_relative_to_run_time/dags/hourly_pageviews.py @@ -0,0 +1,112 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def _extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def _aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + # Airflow 3 maps a bare cron string to CronTriggerTimetable; keep the + # Airflow 2 data interval (the 10:00 run processes 09:00-10:00). + schedule=CronDataIntervalTimetable("0 * * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract_hour = PythonOperator( + task_id="extract_hour", + python_callable=_extract_hour, + op_kwargs={ + "start": "{{ data_interval_start }}", + "end": "{{ data_interval_end }}", + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate_hour = PythonOperator( + task_id="aggregate_hour", + python_callable=_aggregate_hour, + op_kwargs={ + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ (data_interval_start - macros.timedelta(hours=1)).strftime('%Y%m%dT%H') }}", + }, + ) + publish_hour = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ data_interval_start.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ (data_interval_end - macros.timedelta(days=1)).strftime('%Y%m%d') }}T*.csv' -delete" + ), + ) + extract_hour >> aggregate_hour >> publish_hour diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/interval_vars_trigger_timetable/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/interval_vars_trigger_timetable/dags/hourly_pageviews.py new file mode 100644 index 0000000..6a9487d --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/interval_vars_trigger_timetable/dags/hourly_pageviews.py @@ -0,0 +1,110 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def _extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def _aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + schedule="0 * * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract_hour = PythonOperator( + task_id="extract_hour", + python_callable=_extract_hour, + op_kwargs={ + "start": "{{ data_interval_start }}", + "end": "{{ data_interval_end }}", + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate_hour = PythonOperator( + task_id="aggregate_hour", + python_callable=_aggregate_hour, + op_kwargs={ + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ (data_interval_start - macros.timedelta(hours=1)).strftime('%Y%m%dT%H') }}", + }, + ) + publish_hour = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ data_interval_start.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ macros.ds_add(ds, -1) | replace('-', '') }}T*.csv' -delete" + ), + ) + extract_hour >> aggregate_hour >> publish_hour diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/logical_date_bare_cron/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/logical_date_bare_cron/dags/hourly_pageviews.py new file mode 100644 index 0000000..609340f --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/logical_date_bare_cron/dags/hourly_pageviews.py @@ -0,0 +1,110 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def _extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def _aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + schedule="0 * * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract_hour = PythonOperator( + task_id="extract_hour", + python_callable=_extract_hour, + op_kwargs={ + "start": "{{ logical_date }}", + "end": "{{ logical_date + macros.timedelta(hours=1) }}", + "hour": "{{ logical_date.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate_hour = PythonOperator( + task_id="aggregate_hour", + python_callable=_aggregate_hour, + op_kwargs={ + "hour": "{{ logical_date.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ (logical_date - macros.timedelta(hours=1)).strftime('%Y%m%dT%H') }}", + }, + ) + publish_hour = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ logical_date.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ macros.ds_add(ds, -1) | replace('-', '') }}T*.csv' -delete" + ), + ) + extract_hour >> aggregate_hour >> publish_hour diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/ruff_fix_only/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/ruff_fix_only/dags/hourly_pageviews.py new file mode 100644 index 0000000..f567194 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/mutants/ruff_fix_only/dags/hourly_pageviews.py @@ -0,0 +1,110 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + schedule="0 * * * *", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract = PythonOperator( + task_id="extract_hour", + python_callable=extract_hour, + op_kwargs={ + "start": "{{ execution_date }}", + "end": "{{ next_execution_date }}", + "hour": "{{ execution_date.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate = PythonOperator( + task_id="aggregate_hour", + python_callable=aggregate_hour, + op_kwargs={ + "hour": "{{ execution_date.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ prev_execution_date.strftime('%Y%m%dT%H') }}", + }, + ) + publish = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ execution_date.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ yesterday_ds_nodash }}T*.csv' -delete" + ), + ) + extract >> aggregate >> publish diff --git a/evals/airflow/cases/migration-hourly-pageviews-templates/reference/dags/hourly_pageviews.py b/evals/airflow/cases/migration-hourly-pageviews-templates/reference/dags/hourly_pageviews.py new file mode 100644 index 0000000..03ec311 --- /dev/null +++ b/evals/airflow/cases/migration-hourly-pageviews-templates/reference/dags/hourly_pageviews.py @@ -0,0 +1,112 @@ +"""Hourly pageview rollup for the product analytics dashboard. + +At the top of every hour it takes the previous clock hour of raw pageview +events, stages them, aggregates views and unique sessions per page (with the +change against the hour before) and drops a completion marker that the +dashboard loader waits for. Staged extracts are kept for a day: every run +deletes the staging files from the day before its hour. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "pageviews.jsonl" +OUTPUT = PROJECT_ROOT / "output" +STAGING = OUTPUT / "staging" +HOURLY = OUTPUT / "hourly" + + +def _parse(ts: str) -> datetime: + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + + +def _extract_hour(start: str, end: str, hour: str) -> int: + """Stage the raw events with start <= ts < end.""" + lo, hi = _parse(start), _parse(end) + STAGING.mkdir(parents=True, exist_ok=True) + count = 0 + with EVENTS.open() as src, (STAGING / f"{hour}.csv").open("w", newline="") as dst: + writer = csv.writer(dst) + writer.writerow(["ts", "session_id", "page"]) + for line in src: + event = json.loads(line) + if lo <= _parse(event["ts"]) < hi: + writer.writerow([event["ts"], event["session_id"], event["page"]]) + count += 1 + return count + + +def _aggregate_hour(hour: str, prev_hour: str) -> None: + views: dict[str, int] = defaultdict(int) + sessions: dict[str, set[str]] = defaultdict(set) + with (STAGING / f"{hour}.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + sessions[row["page"]].add(row["session_id"]) + + previous: dict[str, int] = {} + prev_file = HOURLY / f"{prev_hour}.csv" + if prev_file.exists(): + with prev_file.open(newline="") as fh: + previous = {row["page"]: int(row["views"]) for row in csv.DictReader(fh)} + + HOURLY.mkdir(parents=True, exist_ok=True) + with (HOURLY / f"{hour}.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views", "sessions", "views_change"]) + for page in sorted(views): + change = views[page] - previous[page] if page in previous else "" + writer.writerow([page, views[page], len(sessions[page]), change]) + + +with DAG( + dag_id="hourly_pageviews", + # Airflow 3 maps a bare cron string to CronTriggerTimetable; keep the + # Airflow 2 data interval (the 10:00 run processes 09:00-10:00). + schedule=CronDataIntervalTimetable("0 * * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "product-analytics", "retries": 2, "retry_delay": timedelta(minutes=2)}, + params={"output": str(OUTPUT)}, + tags=["analytics", "hourly"], +) as dag: + extract_hour = PythonOperator( + task_id="extract_hour", + python_callable=_extract_hour, + op_kwargs={ + "start": "{{ data_interval_start }}", + "end": "{{ data_interval_end }}", + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + }, + ) + aggregate_hour = PythonOperator( + task_id="aggregate_hour", + python_callable=_aggregate_hour, + op_kwargs={ + "hour": "{{ data_interval_start.strftime('%Y%m%dT%H') }}", + "prev_hour": "{{ (data_interval_start - macros.timedelta(hours=1)).strftime('%Y%m%dT%H') }}", + }, + ) + publish_hour = BashOperator( + task_id="publish_hour", + bash_command=( + "mkdir -p {{ params.output }}/markers && " + "echo \"hour={{ data_interval_start.strftime('%Y-%m-%dT%H:00') }} " + "staged={{ ti.xcom_pull(task_ids='extract_hour') }}\" " + "> {{ params.output }}/markers/{{ ts_nodash }}.done && " + "find {{ params.output }}/staging -name '{{ macros.ds_add(ds, -1) | replace('-', '') }}T*.csv' -delete" + ), + ) + extract_hour >> aggregate_hour >> publish_hour diff --git a/evals/airflow/cases/migration-inventory-context/alt_reference/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/alt_reference/dags/inventory_snapshot.py new file mode 100644 index 0000000..f736223 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/alt_reference/dags/inventory_snapshot.py @@ -0,0 +1,74 @@ +"""Nightly inventory snapshot for the fulfilment centre (Airflow 3, TaskFlow). + +Writes stock on hand per SKU at the end of each daily interval with the change +against the previous day's snapshot, then an audit record for alerting. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import pendulum +from airflow.sdk import DeltaDataIntervalTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def read_snapshot(day: str) -> dict[str, int]: + path = OUTPUT_DIR / "inventory" / f"{day}.csv" + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +@dag( + schedule=DeltaDataIntervalTimetable(timedelta(days=1)), + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) +def inventory_snapshot(): + @task(multiple_outputs=True) + def build_snapshot(data_interval_start=None, data_interval_end=None) -> dict: + day = data_interval_start.strftime("%Y-%m-%d") + prev_day = data_interval_start.subtract(days=1).strftime("%Y-%m-%d") + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < data_interval_end: + on_hand[row["sku"]] += int(row["qty"]) + previous = read_snapshot(prev_day) + out = OUTPUT_DIR / "inventory" / f"{day}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + writer.writerow([sku, on_hand[sku], on_hand[sku] - previous[sku] if sku in previous else ""]) + return { + "day": day, + "sku_count": len(on_hand), + "low_stock": sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD), + } + + @task + def audit_snapshot(summary: dict) -> None: + out = OUTPUT_DIR / "audit" / f"{summary['day']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + record = {k: summary[k] for k in ("day", "sku_count", "low_stock")} + out.write_text(json.dumps(record, sort_keys=True)) + + audit_snapshot(build_snapshot()) + + +inventory_snapshot() diff --git a/evals/airflow/cases/migration-inventory-context/case.yaml b/evals/airflow/cases/migration-inventory-context/case.yaml new file mode 100644 index 0000000..0ea8307 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/case.yaml @@ -0,0 +1,25 @@ +id: migration-inventory-context +area: migration +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + Can you upgrade the inventory_snapshot DAG in this repo to Airflow 3.3? Our + new cluster runs 3.3.2 and so does the venv here. The replenishment team's + alerting reads the snapshot CSVs and the audit JSON files, so those need to + keep coming out exactly as they do now. +description: | + Airflow 2.11 -> 3.3 migration of a classic PythonOperator DAG. Pain point P7 + (migration beyond ruff). Traps ruff AIR does not catch: python_callable + parameters named `execution_date` / `prev_ds` (in 3.3 they silently stay None: + one crashes, one silently blanks the day-over-day `change` column), + `get_current_context()["execution_date"]` (KeyError at runtime), and + `ti.xcom_pull(key=...)` without task_ids (returns None on 3.x, so the audit + record loses its counts). The schedule is `timedelta(days=1)`: Airflow 3 + maps a bare timedelta to DeltaTriggerTimetable (not only cron strings + change), so a mechanical `schedule_interval` -> `schedule` rename silently + shifts every snapshot by one day; "@daily" does the same. Primary: imports on 3.3, task graph preserved, dags test + succeeds, two consecutive scheduled runs replayed in 3.3 write the same + snapshot CSVs and audit JSON as the original replayed in 2.11. Secondary: + verification commands, ruff AIR clean, no deprecation warnings. diff --git a/evals/airflow/cases/migration-inventory-context/fixture/.gitignore b/evals/airflow/cases/migration-inventory-context/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/migration-inventory-context/fixture/README.md b/evals/airflow/cases/migration-inventory-context/fixture/README.md new file mode 100644 index 0000000..2392c16 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/fixture/README.md @@ -0,0 +1,10 @@ +# warehouse-ops + +Airflow pipelines for the fulfilment centre. + +- `dags/inventory_snapshot.py`: nightly stock-on-hand snapshot per SKU and the + audit record used by replenishment alerting. +- `data/stock_movements.csv`: movement ledger export (receipts are positive, + shipments negative, timestamps in UTC). + +Outputs land in `output/` (git-ignored). diff --git a/evals/airflow/cases/migration-inventory-context/fixture/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/fixture/dags/inventory_snapshot.py new file mode 100644 index 0000000..fffdfd5 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/fixture/dags/inventory_snapshot.py @@ -0,0 +1,93 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow import DAG +from airflow.operators.python import PythonOperator, get_current_context + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def build_snapshot(ds=None, execution_date=None, prev_ds=None, ti=None, **kwargs): + as_of = execution_date + timedelta(days=1) # end of the day being snapshotted + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_ds) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def audit_snapshot(**kwargs): + context = get_current_context() + day = context["execution_date"].strftime("%Y-%m-%d") + ti = kwargs["ti"] + record = { + "day": day, + "sku_count": ti.xcom_pull(key="sku_count"), + "low_stock": ti.xcom_pull(key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{day}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + schedule_interval=timedelta(days=1), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build = PythonOperator( + task_id="build_snapshot", + python_callable=build_snapshot, + provide_context=True, + ) + audit = PythonOperator( + task_id="audit_snapshot", + python_callable=audit_snapshot, + provide_context=True, + ) + build >> audit diff --git a/evals/airflow/cases/migration-inventory-context/fixture/data/stock_movements.csv b/evals/airflow/cases/migration-inventory-context/fixture/data/stock_movements.csv new file mode 100644 index 0000000..71564be --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/fixture/data/stock_movements.csv @@ -0,0 +1,128 @@ +movement_id,sku,moved_at,qty,reason +M5001,SKU-1001,2026-02-20 08:00:00,83,opening_balance +M5002,SKU-1002,2026-02-20 08:00:00,46,opening_balance +M5003,SKU-1003,2026-02-20 08:00:00,33,opening_balance +M5004,SKU-2001,2026-02-20 08:00:00,108,opening_balance +M5005,SKU-2002,2026-02-20 08:00:00,79,opening_balance +M5006,SKU-3001,2026-02-20 08:00:00,78,opening_balance +M5007,SKU-1002,2026-02-21 00:01:00,58,receipt +M5008,SKU-3001,2026-02-21 00:52:17,-1,shipment +M5009,SKU-1002,2026-02-21 06:43:56,-3,shipment +M5010,SKU-1003,2026-02-21 10:00:59,55,receipt +M5011,SKU-1002,2026-02-21 17:04:55,-10,shipment +M5012,SKU-3001,2026-02-21 17:58:23,-8,shipment +M5013,SKU-1001,2026-02-21 18:46:13,-6,shipment +M5014,SKU-1001,2026-02-21 23:59:30,-3,shipment +M5015,SKU-1003,2026-02-22 00:01:00,-8,shipment +M5016,SKU-1003,2026-02-22 06:06:28,-13,shipment +M5017,SKU-3001,2026-02-22 11:10:44,-5,shipment +M5018,SKU-1002,2026-02-22 11:40:50,-3,shipment +M5019,SKU-1003,2026-02-22 12:14:30,-2,shipment +M5020,SKU-1001,2026-02-22 12:56:43,-14,shipment +M5021,SKU-2001,2026-02-22 14:21:51,-8,shipment +M5022,SKU-2001,2026-02-22 14:41:44,24,receipt +M5023,SKU-1001,2026-02-22 22:06:25,-7,shipment +M5024,SKU-2002,2026-02-22 23:59:30,-4,shipment +M5025,SKU-1003,2026-02-23 00:01:00,-1,shipment +M5026,SKU-2002,2026-02-23 03:04:16,37,receipt +M5027,SKU-1001,2026-02-23 07:52:18,-12,shipment +M5028,SKU-2002,2026-02-23 13:17:16,-11,shipment +M5029,SKU-2002,2026-02-23 13:56:05,26,receipt +M5030,SKU-3001,2026-02-23 19:15:02,-8,shipment +M5031,SKU-1002,2026-02-23 23:59:30,-6,shipment +M5032,SKU-2001,2026-02-24 00:01:00,-14,shipment +M5033,SKU-2001,2026-02-24 00:16:29,-10,shipment +M5034,SKU-1002,2026-02-24 05:54:46,-11,shipment +M5035,SKU-1003,2026-02-24 15:22:34,44,receipt +M5036,SKU-3001,2026-02-24 18:13:41,11,receipt +M5037,SKU-1001,2026-02-24 18:23:05,-14,shipment +M5038,SKU-1002,2026-02-24 23:59:30,39,receipt +M5039,SKU-1003,2026-02-25 00:01:00,39,receipt +M5040,SKU-2001,2026-02-25 05:09:36,-10,shipment +M5041,SKU-1001,2026-02-25 07:33:34,-5,shipment +M5042,SKU-1002,2026-02-25 08:07:42,-6,shipment +M5043,SKU-1001,2026-02-25 11:44:43,-3,shipment +M5044,SKU-3001,2026-02-25 13:27:51,-3,shipment +M5045,SKU-1002,2026-02-25 23:59:30,-3,shipment +M5046,SKU-3001,2026-02-26 00:01:00,-3,shipment +M5047,SKU-1003,2026-02-26 00:56:32,-12,shipment +M5048,SKU-2002,2026-02-26 03:41:41,-4,shipment +M5049,SKU-2002,2026-02-26 13:35:13,-8,shipment +M5050,SKU-2001,2026-02-26 20:53:17,40,receipt +M5051,SKU-1001,2026-02-26 23:59:30,-14,shipment +M5052,SKU-2001,2026-02-27 00:01:00,-7,shipment +M5053,SKU-1002,2026-02-27 02:11:29,-13,shipment +M5054,SKU-1003,2026-02-27 09:35:22,-1,shipment +M5055,SKU-3001,2026-02-27 11:10:39,-6,shipment +M5056,SKU-2001,2026-02-27 14:34:18,-3,shipment +M5057,SKU-1002,2026-02-27 18:12:33,-6,shipment +M5058,SKU-2001,2026-02-27 23:59:30,-4,shipment +M5059,SKU-3001,2026-02-28 00:01:00,-13,shipment +M5060,SKU-1002,2026-02-28 03:47:13,-4,shipment +M5061,SKU-3001,2026-02-28 10:30:24,29,receipt +M5062,SKU-1001,2026-02-28 12:57:08,-8,shipment +M5063,SKU-1001,2026-02-28 17:58:26,-12,shipment +M5064,SKU-1002,2026-02-28 18:51:34,-2,shipment +M5065,SKU-1001,2026-02-28 23:59:30,-11,shipment +M5066,SKU-1001,2026-03-01 00:01:00,-10,shipment +M5067,SKU-2002,2026-03-01 05:06:44,-7,shipment +M5068,SKU-2001,2026-03-01 08:14:15,50,receipt +M5069,SKU-2002,2026-03-01 08:55:19,-5,shipment +M5070,SKU-2002,2026-03-01 22:27:51,38,receipt +M5071,SKU-3001,2026-03-01 23:59:30,-2,shipment +M5072,SKU-1003,2026-03-02 00:01:00,53,receipt +M5073,SKU-2001,2026-03-02 05:24:32,-9,shipment +M5074,SKU-1001,2026-03-02 07:17:31,37,receipt +M5075,SKU-3001,2026-03-02 11:12:00,40,receipt +M5076,SKU-3001,2026-03-02 16:43:54,-8,shipment +M5077,SKU-1001,2026-03-02 23:59:30,-11,shipment +M5078,SKU-1003,2026-03-03 00:01:00,-8,shipment +M5079,SKU-1003,2026-03-03 01:55:31,-3,shipment +M5080,SKU-3001,2026-03-03 08:39:53,50,receipt +M5081,SKU-3001,2026-03-03 09:48:56,29,receipt +M5082,SKU-2001,2026-03-03 13:26:05,11,receipt +M5083,SKU-1003,2026-03-03 15:58:14,-14,shipment +M5084,SKU-3001,2026-03-03 16:18:16,-2,shipment +M5085,SKU-1002,2026-03-03 23:59:30,43,receipt +M5086,SKU-1003,2026-03-04 00:01:00,-11,shipment +M5087,SKU-1001,2026-03-04 03:18:34,-2,shipment +M5088,SKU-2002,2026-03-04 04:28:09,-8,shipment +M5089,SKU-1001,2026-03-04 05:51:07,-12,shipment +M5090,SKU-2002,2026-03-04 06:22:53,24,receipt +M5091,SKU-2001,2026-03-04 20:43:14,45,receipt +M5092,SKU-2001,2026-03-04 23:59:30,-5,shipment +M5093,SKU-1002,2026-03-05 00:01:00,-4,shipment +M5094,SKU-1002,2026-03-05 02:18:47,-4,shipment +M5095,SKU-1003,2026-03-05 03:47:22,-1,shipment +M5096,SKU-2002,2026-03-05 07:14:21,21,receipt +M5097,SKU-2001,2026-03-05 10:57:10,-3,shipment +M5098,SKU-1003,2026-03-05 19:07:30,-14,shipment +M5099,SKU-1003,2026-03-05 19:42:04,24,receipt +M5100,SKU-2002,2026-03-05 23:29:48,15,receipt +M5101,SKU-2001,2026-03-05 23:59:30,10,receipt +M5102,SKU-1003,2026-03-06 00:01:00,-5,shipment +M5103,SKU-3001,2026-03-06 01:03:02,-4,shipment +M5104,SKU-2001,2026-03-06 08:01:42,-2,shipment +M5105,SKU-2002,2026-03-06 12:30:29,-14,shipment +M5106,SKU-1002,2026-03-06 12:37:15,-8,shipment +M5107,SKU-2001,2026-03-06 12:45:42,-6,shipment +M5108,SKU-3001,2026-03-06 15:24:49,-7,shipment +M5109,SKU-2001,2026-03-06 16:46:14,28,receipt +M5110,SKU-3001,2026-03-06 20:02:23,-2,shipment +M5111,SKU-3001,2026-03-06 23:59:30,-11,shipment +M5112,SKU-2002,2026-03-07 00:01:00,18,receipt +M5113,SKU-1003,2026-03-07 02:08:35,-14,shipment +M5114,SKU-1002,2026-03-07 09:15:35,-8,shipment +M5115,SKU-1003,2026-03-07 12:58:39,-8,shipment +M5116,SKU-1001,2026-03-07 14:53:01,-2,shipment +M5117,SKU-3001,2026-03-07 18:27:15,-1,shipment +M5118,SKU-1003,2026-03-07 21:23:57,-7,shipment +M5119,SKU-1003,2026-03-07 23:53:28,-5,shipment +M5120,SKU-1003,2026-03-07 23:59:30,-8,shipment +M5121,SKU-3001,2026-03-08 00:01:00,-2,shipment +M5122,SKU-3001,2026-03-08 01:52:37,-8,shipment +M5123,SKU-3001,2026-03-08 03:38:52,32,receipt +M5124,SKU-1002,2026-03-08 14:24:15,37,receipt +M5125,SKU-1001,2026-03-08 16:08:02,12,receipt +M5126,SKU-1002,2026-03-08 19:26:14,-5,shipment +M5127,SKU-2002,2026-03-08 23:59:30,-9,shipment diff --git a/evals/airflow/cases/migration-inventory-context/fixture/requirements.txt b/evals/airflow/cases/migration-inventory-context/fixture/requirements.txt new file mode 100644 index 0000000..cb9ccf0 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==2.11.2 diff --git a/evals/airflow/cases/migration-inventory-context/grade.py b/evals/airflow/cases/migration-inventory-context/grade.py new file mode 100644 index 0000000..ddcad48 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/grade.py @@ -0,0 +1,86 @@ +"""Grader for migration-inventory-context. + +The fixture is an Airflow 2.11 project using removed context keys in ways ruff +does not see (callable kwargs ``execution_date``/``prev_ds``, +``get_current_context()["execution_date"]``, ``xcom_pull`` without +``task_ids``). Behaviour is compared with the ORIGINAL project replayed in the +2.11 env: the scheduled runs that fire at 2026-03-05 00:00 and 2026-03-06 00:00 +UTC run back to back against one DB (the second snapshot's ``change`` column +depends on the first) and must write the same snapshot CSVs and audit records. +""" + +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import migsim # noqa: E402 + +g = migsim.g +DAG_ID = "inventory_snapshot" +RUN_AFTERS = ["2026-03-05T00:00:00+00:00", "2026-03-06T00:00:00+00:00"] +EXPECTED_TASKS = {"build_snapshot", "audit_snapshot"} +EXPECTED_DEPS = [["build_snapshot", "audit_snapshot"]] + + +def normalise(snap: dict[str, str]) -> dict[str, str]: + out = {} + for path, text in snap.items(): + if path.endswith(".json"): + try: + text = json.dumps(json.loads(text), sort_keys=True) + except ValueError: + text = f" {text[:200]}" + out[path] = text + return out + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + orig_ws, orig, orig_log = migsim.run_original(CASE_DIR, DAG_ID, RUN_AFTERS) + if not migsim.runs_ok(orig): + raise SystemExit(f"case bug: original project failed in 2.11: {migsim.describe(orig)}\n{orig_log}") + expected = normalise(migsim.snapshot(orig_ws)) + if not any(p.startswith("audit/") for p in expected): + raise SystemExit(f"case bug: original wrote no audit records: {expected}") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly on Airflow 3.3", imp["ok"] and not imp["import_errors"] and dag is not None, + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"dags: {sorted(imp['dags'])}") + grader.primary("inventory_snapshot keeps its tasks and dependency", + dag is not None and set(dag["tasks"]) == EXPECTED_TASKS and dag["deps"] == EXPECTED_DEPS, + f"tasks={dag and dag['tasks']} deps={dag and dag['deps']}") + + if dag is None: + grader.primary("dags test succeeds", False, "DAG missing") + else: + shutil.rmtree(ws / "output", ignore_errors=True) + cli, cli_log = migsim.simulate(py, ws, DAG_ID, [migsim.manual("2026-03-05")]) + grader.primary("dags test succeeds", migsim.runs_ok(cli), + migsim.describe(cli) + ("" if migsim.runs_ok(cli) else "\n" + cli_log[-2500:])) + + shutil.rmtree(ws / "output", ignore_errors=True) + sim, sim_log = migsim.simulate(py, ws, DAG_ID, RUN_AFTERS, block_orm=True) + grader.primary("consecutive scheduled daily runs succeed on 3.3", migsim.runs_ok(sim), + migsim.describe(sim) + ("" if migsim.runs_ok(sim) else "\n" + sim_log[-2500:])) + got = normalise(migsim.snapshot(ws)) + grader.primary("snapshots and audit records match the Airflow 2 output", got == expected, + f"expected {expected}\n got {got}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/migration-inventory-context/migsim.py b/evals/airflow/cases/migration-inventory-context/migsim.py new file mode 100644 index 0000000..ffa3509 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/migsim.py @@ -0,0 +1,260 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +This file is identical in every ``migration-*`` case directory. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + dag = load_dag(P["dag_id"]) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + tt = core_timetable(dag) + RESULT["timetable"] = type(tt).__name__ + for spec in P["runs"]: + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(P["dag_id"], logical) + forgive_deadline_bug(row) + continue + run_after = spec + row = {"run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {P['dag_id']} fires at {run_after} ({type(tt).__name__})" + continue + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(P["dag_id"], info.logical_date) + forgive_deadline_bug(row) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is either an ISO datetime T (the *scheduled* run that fires at T, + with the logical date and data interval the DAG's timetable gives it) or + ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), "dag_id": dag_id, + "runs": list(runs), "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + return "; ".join( + f"{r.get('run_after') or 'manual ' + str(r.get('manual'))}: " + (r.get("error") or f"state={r.get('state')} interval=[{r.get('start')}, {r.get('end')}]" + f" failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log diff --git a/evals/airflow/cases/migration-inventory-context/mutants/daily_cron_preset/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/mutants/daily_cron_preset/dags/inventory_snapshot.py new file mode 100644 index 0000000..2ee578e --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/mutants/daily_cron_preset/dags/inventory_snapshot.py @@ -0,0 +1,87 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def _build_snapshot(ds, data_interval_end, ti, **kwargs): + # Airflow 3 removed execution_date/prev_ds from the context. The interval is + # one day (timedelta schedule), so the old ``execution_date + 1 day`` is the + # interval end and ``prev_ds`` is the day before ``ds``. + as_of = data_interval_end + prev_day = (datetime.strptime(ds, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d") + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_day) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def _audit_snapshot(ds, ti, **kwargs): + # Airflow 3: xcom_pull without task_ids only looks at the current task. + record = { + "day": ds, + "sku_count": ti.xcom_pull(task_ids="build_snapshot", key="sku_count"), + "low_stock": ti.xcom_pull(task_ids="build_snapshot", key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + schedule="@daily", + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=_build_snapshot) + audit_snapshot = PythonOperator(task_id="audit_snapshot", python_callable=_audit_snapshot) + build_snapshot >> audit_snapshot diff --git a/evals/airflow/cases/migration-inventory-context/mutants/prev_ds_default_none/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/mutants/prev_ds_default_none/dags/inventory_snapshot.py new file mode 100644 index 0000000..b0fdac3 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/mutants/prev_ds_default_none/dags/inventory_snapshot.py @@ -0,0 +1,88 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def _build_snapshot(ds, data_interval_end, ti, prev_ds=None, **kwargs): + # Airflow 3 removed execution_date/prev_ds from the context. The interval is + # one day (timedelta schedule), so the old ``execution_date + 1 day`` is the + # interval end and ``prev_ds`` is the day before ``ds``. + as_of = data_interval_end + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_ds) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def _audit_snapshot(ds, ti, **kwargs): + # Airflow 3: xcom_pull without task_ids only looks at the current task. + record = { + "day": ds, + "sku_count": ti.xcom_pull(task_ids="build_snapshot", key="sku_count"), + "low_stock": ti.xcom_pull(task_ids="build_snapshot", key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + # Airflow 3 turns a bare timedelta into DeltaTriggerTimetable (logical date = + # the tick). Keep the Airflow 2 data-interval semantics explicitly. + schedule=DeltaDataIntervalTimetable(timedelta(days=1)), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=_build_snapshot) + audit_snapshot = PythonOperator(task_id="audit_snapshot", python_callable=_audit_snapshot) + build_snapshot >> audit_snapshot diff --git a/evals/airflow/cases/migration-inventory-context/mutants/timedelta_schedule_kept/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/mutants/timedelta_schedule_kept/dags/inventory_snapshot.py new file mode 100644 index 0000000..2ad8c80 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/mutants/timedelta_schedule_kept/dags/inventory_snapshot.py @@ -0,0 +1,87 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def _build_snapshot(ds, data_interval_end, ti, **kwargs): + # Airflow 3 removed execution_date/prev_ds from the context. The interval is + # one day (timedelta schedule), so the old ``execution_date + 1 day`` is the + # interval end and ``prev_ds`` is the day before ``ds``. + as_of = data_interval_end + prev_day = (datetime.strptime(ds, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d") + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_day) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def _audit_snapshot(ds, ti, **kwargs): + # Airflow 3: xcom_pull without task_ids only looks at the current task. + record = { + "day": ds, + "sku_count": ti.xcom_pull(task_ids="build_snapshot", key="sku_count"), + "low_stock": ti.xcom_pull(task_ids="build_snapshot", key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + schedule=timedelta(days=1), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=_build_snapshot) + audit_snapshot = PythonOperator(task_id="audit_snapshot", python_callable=_audit_snapshot) + build_snapshot >> audit_snapshot diff --git a/evals/airflow/cases/migration-inventory-context/mutants/xcom_pull_without_task_ids/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/mutants/xcom_pull_without_task_ids/dags/inventory_snapshot.py new file mode 100644 index 0000000..aa1f37b --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/mutants/xcom_pull_without_task_ids/dags/inventory_snapshot.py @@ -0,0 +1,89 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def _build_snapshot(ds, data_interval_end, ti, **kwargs): + # Airflow 3 removed execution_date/prev_ds from the context. The interval is + # one day (timedelta schedule), so the old ``execution_date + 1 day`` is the + # interval end and ``prev_ds`` is the day before ``ds``. + as_of = data_interval_end + prev_day = (datetime.strptime(ds, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d") + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_day) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def _audit_snapshot(ds, ti, **kwargs): + # Airflow 3: xcom_pull without task_ids only looks at the current task. + record = { + "day": ds, + "sku_count": ti.xcom_pull(key="sku_count"), + "low_stock": ti.xcom_pull(key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + # Airflow 3 turns a bare timedelta into DeltaTriggerTimetable (logical date = + # the tick). Keep the Airflow 2 data-interval semantics explicitly. + schedule=DeltaDataIntervalTimetable(timedelta(days=1)), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=_build_snapshot) + audit_snapshot = PythonOperator(task_id="audit_snapshot", python_callable=_audit_snapshot) + build_snapshot >> audit_snapshot diff --git a/evals/airflow/cases/migration-inventory-context/reference/dags/inventory_snapshot.py b/evals/airflow/cases/migration-inventory-context/reference/dags/inventory_snapshot.py new file mode 100644 index 0000000..0109805 --- /dev/null +++ b/evals/airflow/cases/migration-inventory-context/reference/dags/inventory_snapshot.py @@ -0,0 +1,89 @@ +"""Nightly inventory snapshot for the fulfilment centre. + +For every daily interval it writes the stock on hand per SKU at the END of the +interval (``output/inventory/.csv``), with the change against the previous +day's snapshot, and then an audit record (``output/audit/.json``) that the +replenishment team's alerting reads. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, DeltaDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +MOVEMENTS = PROJECT_ROOT / "data" / "stock_movements.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +LOW_STOCK_THRESHOLD = 20 + + +def _snapshot_path(day: str) -> Path: + return OUTPUT_DIR / "inventory" / f"{day}.csv" + + +def _read_snapshot(day: str) -> dict[str, int]: + path = _snapshot_path(day) + if not path.exists(): + return {} + with path.open(newline="") as fh: + return {row["sku"]: int(row["on_hand"]) for row in csv.DictReader(fh)} + + +def _build_snapshot(ds, data_interval_end, ti, **kwargs): + # Airflow 3 removed execution_date/prev_ds from the context. The interval is + # one day (timedelta schedule), so the old ``execution_date + 1 day`` is the + # interval end and ``prev_ds`` is the day before ``ds``. + as_of = data_interval_end + prev_day = (datetime.strptime(ds, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d") + on_hand: dict[str, int] = defaultdict(int) + with MOVEMENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + moved_at = datetime.fromisoformat(row["moved_at"]).replace(tzinfo=timezone.utc) + if moved_at < as_of: + on_hand[row["sku"]] += int(row["qty"]) + + previous = _read_snapshot(prev_day) + out = _snapshot_path(ds) + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["sku", "on_hand", "change"]) + for sku in sorted(on_hand): + change = on_hand[sku] - previous[sku] if sku in previous else "" + writer.writerow([sku, on_hand[sku], change]) + + ti.xcom_push(key="sku_count", value=len(on_hand)) + ti.xcom_push(key="low_stock", value=sorted(s for s, q in on_hand.items() if q < LOW_STOCK_THRESHOLD)) + + +def _audit_snapshot(ds, ti, **kwargs): + # Airflow 3: xcom_pull without task_ids only looks at the current task. + record = { + "day": ds, + "sku_count": ti.xcom_pull(task_ids="build_snapshot", key="sku_count"), + "low_stock": ti.xcom_pull(task_ids="build_snapshot", key="low_stock"), + } + out = OUTPUT_DIR / "audit" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(record, sort_keys=True)) + + +with DAG( + dag_id="inventory_snapshot", + # Airflow 3 turns a bare timedelta into DeltaTriggerTimetable (logical date = + # the tick). Keep the Airflow 2 data-interval semantics explicitly. + schedule=DeltaDataIntervalTimetable(timedelta(days=1)), + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "warehouse-ops", "retries": 1, "retry_delay": timedelta(minutes=5)}, + tags=["inventory"], +) as dag: + build_snapshot = PythonOperator(task_id="build_snapshot", python_callable=_build_snapshot) + audit_snapshot = PythonOperator(task_id="audit_snapshot", python_callable=_audit_snapshot) + build_snapshot >> audit_snapshot diff --git a/evals/airflow/cases/migration-revenue-previous-day/alt_reference/_DELETE b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/_DELETE new file mode 100644 index 0000000..799ac8a --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/_DELETE @@ -0,0 +1 @@ +dags/sql/daily_revenue.sql diff --git a/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/daily_revenue.py b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/daily_revenue.py new file mode 100644 index 0000000..4feb1ef --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/daily_revenue.py @@ -0,0 +1,73 @@ +"""Daily revenue per region (Airflow 3, TaskFlow). + +The DAG fires at 04:15 UTC every day and reports on the previous UTC business +day. Under a cron trigger the logical date is the fire time itself, so the +business day is derived explicitly as ``logical_date - 1 day``. +""" + +from __future__ import annotations + +import csv +import json +from datetime import timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_PATH = PROJECT_ROOT / "data" / "orders.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + +QUERY = """ +SELECT region, COUNT(*) AS orders, CAST(SUM(amount) AS DOUBLE) AS revenue +FROM read_csv(?, header = true, columns = { + 'order_id': 'VARCHAR', 'order_ts': 'TIMESTAMP', 'region': 'VARCHAR', + 'amount': 'DECIMAL(12, 2)', 'status': 'VARCHAR'}) +WHERE status = 'completed' AND order_ts >= ? AND order_ts < ? +GROUP BY region +ORDER BY region +""" + + +def business_day(logical_date) -> pendulum.Date: + return pendulum.instance(logical_date).in_timezone("UTC").date().subtract(days=1) + + +@dag( + dag_id="daily_revenue", + schedule=CronTriggerTimetable("15 4 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["finance", "revenue"], +) +def daily_revenue(): + @task(task_id="load_daily_revenue") + def load(logical_date=None) -> str: + day = business_day(logical_date) + start = pendulum.datetime(day.year, day.month, day.day) + rows = duckdb.execute( + QUERY, [str(ORDERS_PATH), start.naive(), start.add(days=1).naive()] + ).fetchall() + out = OUTPUT_DIR / "daily_revenue" / f"{day.isoformat()}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region", "orders", "revenue"]) + for region, orders, revenue in rows: + writer.writerow([region, orders, f"{revenue:.2f}"]) + return day.isoformat() + + @task(task_id="publish_manifest") + def publish(day: str, logical_date=None) -> None: + out = OUTPUT_DIR / "manifests" / f"{day}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"business_date": day, "logical_ts": str(logical_date)})) + + publish(load()) + + +daily_revenue() diff --git a/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/region_reference.py b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/region_reference.py new file mode 100644 index 0000000..2c31494 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/dags/region_reference.py @@ -0,0 +1,31 @@ +"""Rebuild the region reference file on demand (triggered from the UI).""" + +from __future__ import annotations + +import csv +from datetime import datetime +from pathlib import Path + +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag(schedule=None, start_date=datetime(2026, 1, 1), catchup=False, tags=["finance", "reference"]) +def region_reference(): + @task + def write_regions(): + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + regions = sorted({row["region"] for row in csv.DictReader(fh)}) + out = PROJECT_ROOT / "output" / "reference" / "regions.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region"]) + writer.writerows([r] for r in regions) + + EmptyOperator(task_id="start") >> write_regions() + + +region_reference() diff --git a/evals/airflow/cases/migration-revenue-previous-day/alt_reference/tests/test_dag_integrity.py b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/tests/test_dag_integrity.py new file mode 100644 index 0000000..c1d2e41 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/alt_reference/tests/test_dag_integrity.py @@ -0,0 +1,23 @@ +"""CI checks: every DAG imports and the revenue pipeline keeps its shape.""" + +import os +from pathlib import Path + +from airflow.models import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +os.environ.setdefault("AIRFLOW__CORE__LOAD_EXAMPLES", "False") + + +def _bag(): + return DagBag(dag_folder=str(DAGS_DIR)) + + +def test_no_import_errors(): + assert _bag().import_errors == {} + + +def test_daily_revenue_structure(): + dag = _bag().dags["daily_revenue"] + assert set(dag.task_ids) == {"load_daily_revenue", "publish_manifest"} + assert dag.get_task("publish_manifest").upstream_task_ids == {"load_daily_revenue"} diff --git a/evals/airflow/cases/migration-revenue-previous-day/case.yaml b/evals/airflow/cases/migration-revenue-previous-day/case.yaml new file mode 100644 index 0000000..d0eef26 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/case.yaml @@ -0,0 +1,28 @@ +id: migration-revenue-previous-day +area: migration +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + We're moving our Airflow deployment from 2.11 to 3.3 next week and this repo + has to be ready. The local virtualenv is already on apache-airflow 3.3.2. + Please migrate the DAGs (and whatever else in the repo needs it) so everything + works on Airflow 3.3. Finance depends on daily_revenue, so it must keep + producing exactly the same reports as it does today. +description: | + Airflow 2.11 -> 3.3 migration of a finance project. Pain point P7/P2 + (migration beyond ruff; cron semantics). `daily_revenue` runs at 04:15 UTC + with `schedule_interval="15 4 * * *"` and reports on the PREVIOUS business + day via `{{ ds }}`/`{{ next_ds }}` (data-interval semantics). Traps ruff AIR + does not catch: `{{ next_ds }}` inside a .sql template, `{{ execution_date }}` + inside a BashOperator command, and the Airflow 3 change that maps a bare cron + string to CronTriggerTimetable (the 04:15 run on day D would suddenly report + on day D). Also DummyOperator/days_ago in a second DAG and a 2.x-style + `DagBag(include_examples=False)` test. Primary: imports on 3.3, task graph + preserved, `airflow dags test` succeeds, the scheduled 04:15 runs replayed in + 3.3 write byte-identical revenue CSVs and the same manifest business_date as + the original project replayed in 2.11, and the repo's pytest suite passes + and still fails when a planted DAG file does not import (a skipped or + gutted suite does not count). + Secondary: verification commands, ruff AIR clean, no deprecation warnings. diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/README.md b/evals/airflow/cases/migration-revenue-previous-day/fixture/README.md new file mode 100644 index 0000000..f74373e --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/README.md @@ -0,0 +1,15 @@ +# finance-pipelines + +Airflow DAGs for the finance team. + +| DAG | Schedule | What it does | +|---|---|---| +| `daily_revenue` | 04:15 UTC daily | Completed-order revenue per region for the previous UTC business day, written to `output/daily_revenue/.csv`, plus `output/manifests/.json` for the dashboard | +| `region_reference` | manual | Rebuilds `output/reference/regions.csv` | + +Local checks: + +```bash +pytest tests/ +airflow dags test daily_revenue 2026-03-04 +``` diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/daily_revenue.py b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/daily_revenue.py new file mode 100644 index 0000000..d013cbd --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/daily_revenue.py @@ -0,0 +1,71 @@ +"""Daily revenue per region. + +Runs every morning at 04:15 UTC, once the late order events for the previous +business day (UTC) have landed, and writes one CSV per business day plus a +small manifest the finance dashboard polls for. +""" + +from __future__ import annotations + +import csv +from datetime import timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow import DAG +from airflow.operators.bash import BashOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_PATH = PROJECT_ROOT / "data" / "orders.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + +default_args = { + "owner": "finance-data", + "retries": 2, + "retry_delay": timedelta(minutes=10), +} + + +def load_daily_revenue(templates_dict, ds, **_): + """Run the rendered query and overwrite the CSV for the business day.""" + rows = duckdb.sql(templates_dict["query"]).fetchall() + out = OUTPUT_DIR / "daily_revenue" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region", "orders", "revenue"]) + for region, orders, revenue in rows: + writer.writerow([region, orders, f"{revenue:.2f}"]) + return str(out) + + +with DAG( + dag_id="daily_revenue", + schedule_interval="15 4 * * *", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args=default_args, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"orders_path": str(ORDERS_PATH), "output_dir": str(OUTPUT_DIR)}, + tags=["finance", "revenue"], +) as dag: + load = PythonOperator( + task_id="load_daily_revenue", + python_callable=load_daily_revenue, + templates_dict={"query": "daily_revenue.sql"}, + templates_exts=[".sql"], + ) + + publish = BashOperator( + task_id="publish_manifest", + bash_command=( + "mkdir -p {{ params.output_dir }}/manifests && " + "echo '{\"business_date\": \"{{ ds }}\", \"logical_ts\": \"{{ execution_date }}\"}' " + "> {{ params.output_dir }}/manifests/{{ ds }}.json" + ), + ) + + load >> publish diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/region_reference.py b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/region_reference.py new file mode 100644 index 0000000..584b13a --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/region_reference.py @@ -0,0 +1,36 @@ +"""Rebuild the region reference file on demand (triggered from the UI).""" + +from __future__ import annotations + +import csv +from pathlib import Path + +from airflow import DAG +from airflow.operators.dummy import DummyOperator +from airflow.operators.python import PythonOperator +from airflow.utils.dates import days_ago + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def write_regions(): + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + regions = sorted({row["region"] for row in csv.DictReader(fh)}) + out = PROJECT_ROOT / "output" / "reference" / "regions.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region"]) + writer.writerows([r] for r in regions) + + +with DAG( + dag_id="region_reference", + schedule_interval=None, + start_date=days_ago(1), + catchup=False, + tags=["finance", "reference"], +) as dag: + start = DummyOperator(task_id="start") + write = PythonOperator(task_id="write_regions", python_callable=write_regions) + start >> write diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/sql/daily_revenue.sql b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/sql/daily_revenue.sql new file mode 100644 index 0000000..7acfdb5 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/dags/sql/daily_revenue.sql @@ -0,0 +1,21 @@ +-- Completed-order revenue per region for one UTC business day. +SELECT + region, + COUNT(*) AS orders, + CAST(SUM(amount) AS DOUBLE) AS revenue +FROM read_csv( + '{{ params.orders_path }}', + header = true, + columns = { + 'order_id': 'VARCHAR', + 'order_ts': 'TIMESTAMP', + 'region': 'VARCHAR', + 'amount': 'DECIMAL(12, 2)', + 'status': 'VARCHAR' + } +) +WHERE status = 'completed' + AND order_ts >= TIMESTAMP '{{ ds }} 00:00:00' + AND order_ts < TIMESTAMP '{{ next_ds }} 00:00:00' +GROUP BY region +ORDER BY region diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/data/orders.csv b/evals/airflow/cases/migration-revenue-previous-day/fixture/data/orders.csv new file mode 100644 index 0000000..c908fe1 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/data/orders.csv @@ -0,0 +1,73 @@ +order_id,order_ts,region,amount,status +O-100204,2026-03-02 00:03:00,APAC,98.65,completed +O-100206,2026-03-02 01:50:30,APAC,134.99,completed +O-100211,2026-03-02 02:40:00,LATAM,104.03,cancelled +O-100215,2026-03-02 02:47:09,EMEA,100.28,completed +O-100223,2026-03-02 04:14:59,EMEA,245.01,cancelled +O-100229,2026-03-02 04:57:47,NA,99.43,cancelled +O-100232,2026-03-02 13:36:42,APAC,196.32,completed +O-100233,2026-03-02 13:58:38,NA,7.32,completed +O-100234,2026-03-02 14:23:39,EMEA,106.97,completed +O-100241,2026-03-02 16:07:08,APAC,160.33,completed +O-100248,2026-03-02 19:58:00,NA,143.60,cancelled +O-100250,2026-03-02 23:58:00,APAC,197.65,completed +O-100252,2026-03-03 00:03:00,APAC,194.46,refunded +O-100253,2026-03-03 02:26:23,NA,9.27,cancelled +O-100256,2026-03-03 02:33:53,EMEA,150.88,completed +O-100262,2026-03-03 02:40:00,NA,218.37,completed +O-100265,2026-03-03 04:14:59,NA,196.34,completed +O-100272,2026-03-03 05:09:51,NA,103.86,completed +O-100276,2026-03-03 09:49:34,EMEA,169.22,completed +O-100277,2026-03-03 12:11:45,NA,11.41,refunded +O-100280,2026-03-03 17:23:36,NA,228.63,completed +O-100282,2026-03-03 19:51:43,NA,176.85,completed +O-100289,2026-03-03 22:06:15,APAC,64.32,completed +O-100292,2026-03-03 23:58:00,APAC,221.14,completed +O-100300,2026-03-04 00:03:00,APAC,249.86,completed +O-100305,2026-03-04 00:13:57,APAC,208.79,refunded +O-100309,2026-03-04 02:40:00,APAC,110.07,completed +O-100311,2026-03-04 03:00:17,NA,232.78,completed +O-100312,2026-03-04 04:14:59,NA,143.33,completed +O-100316,2026-03-04 09:54:35,APAC,162.09,completed +O-100323,2026-03-04 17:39:03,APAC,30.42,completed +O-100330,2026-03-04 18:14:59,NA,161.09,completed +O-100332,2026-03-04 18:34:18,EMEA,230.51,completed +O-100338,2026-03-04 18:39:18,EMEA,178.89,completed +O-100344,2026-03-04 18:56:01,EMEA,20.09,refunded +O-100347,2026-03-04 23:58:00,LATAM,116.19,completed +O-100351,2026-03-05 00:03:00,NA,33.66,completed +O-100354,2026-03-05 02:03:15,APAC,116.82,refunded +O-100357,2026-03-05 02:40:00,NA,154.65,completed +O-100359,2026-03-05 03:40:57,NA,225.01,completed +O-100361,2026-03-05 04:14:59,LATAM,135.62,completed +O-100368,2026-03-05 05:04:05,NA,198.53,completed +O-100375,2026-03-05 05:10:52,APAC,185.07,cancelled +O-100381,2026-03-05 05:43:40,NA,198.02,completed +O-100389,2026-03-05 07:48:49,EMEA,182.43,completed +O-100391,2026-03-05 19:13:44,LATAM,191.73,cancelled +O-100397,2026-03-05 21:52:50,LATAM,213.78,completed +O-100400,2026-03-05 23:58:00,EMEA,45.68,completed +O-100405,2026-03-06 00:03:00,LATAM,116.43,completed +O-100406,2026-03-06 02:40:00,LATAM,104.56,completed +O-100408,2026-03-06 03:20:24,EMEA,132.21,completed +O-100412,2026-03-06 03:23:14,NA,89.83,completed +O-100416,2026-03-06 04:14:59,EMEA,103.43,completed +O-100417,2026-03-06 12:23:20,EMEA,181.76,refunded +O-100422,2026-03-06 12:39:19,APAC,39.41,completed +O-100425,2026-03-06 13:42:50,NA,148.84,completed +O-100430,2026-03-06 16:32:15,LATAM,161.34,completed +O-100435,2026-03-06 18:19:10,EMEA,211.65,completed +O-100443,2026-03-06 23:38:34,LATAM,209.52,completed +O-100450,2026-03-06 23:58:00,LATAM,41.27,completed +O-100454,2026-03-07 00:03:00,NA,65.66,completed +O-100458,2026-03-07 00:38:59,LATAM,116.72,completed +O-100464,2026-03-07 02:40:00,EMEA,205.43,completed +O-100468,2026-03-07 02:51:39,NA,84.75,completed +O-100472,2026-03-07 03:33:47,EMEA,80.89,completed +O-100473,2026-03-07 04:14:59,LATAM,188.91,completed +O-100481,2026-03-07 06:23:21,NA,185.57,completed +O-100484,2026-03-07 12:20:15,LATAM,224.39,completed +O-100487,2026-03-07 17:11:45,LATAM,6.11,completed +O-100490,2026-03-07 17:48:10,LATAM,96.80,completed +O-100494,2026-03-07 20:41:09,EMEA,209.65,completed +O-100498,2026-03-07 23:58:00,EMEA,155.09,completed diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/requirements.txt b/evals/airflow/cases/migration-revenue-previous-day/fixture/requirements.txt new file mode 100644 index 0000000..74b599a --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/requirements.txt @@ -0,0 +1,4 @@ +apache-airflow==2.11.2 +duckdb==1.4.4 +pendulum==3.2.0 +pytest==8.4.2 diff --git a/evals/airflow/cases/migration-revenue-previous-day/fixture/tests/test_dag_integrity.py b/evals/airflow/cases/migration-revenue-previous-day/fixture/tests/test_dag_integrity.py new file mode 100644 index 0000000..00cc214 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/fixture/tests/test_dag_integrity.py @@ -0,0 +1,24 @@ +"""CI checks: every DAG imports and the revenue pipeline keeps its shape.""" + +from pathlib import Path + +import pytest +from airflow.models import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" + + +@pytest.fixture(scope="session") +def dagbag(): + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {} + + +def test_daily_revenue_structure(dagbag): + dag = dagbag.get_dag("daily_revenue") + assert dag is not None + assert set(dag.task_ids) == {"load_daily_revenue", "publish_manifest"} + assert dag.get_task("publish_manifest").upstream_task_ids == {"load_daily_revenue"} diff --git a/evals/airflow/cases/migration-revenue-previous-day/grade.py b/evals/airflow/cases/migration-revenue-previous-day/grade.py new file mode 100644 index 0000000..a36e0f3 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/grade.py @@ -0,0 +1,133 @@ +"""Grader for migration-revenue-previous-day. + +The fixture is an Airflow 2.11 project; the agent migrates it to 3.3. Behaviour +is compared against the ORIGINAL project executed in the 2.11 env: + +- the scheduled runs that fire at 04:15 UTC on 2026-03-05 and 2026-03-06 are + replayed in both envs (``migsim``) and must write the same files with the same + revenue figures (same business day: the day BEFORE the fire time); +- the DAGs import in 3.3, ``airflow dags test`` succeeds, the task graph is + preserved and the project's own pytest suite passes on 3.3; +- the migrated suite is still a real CI gate: with a planted DAG file that does + not import, it must fail (a skipped or gutted suite passes vacuously). +""" + +from __future__ import annotations + +import json +import shutil +import sys +import tempfile +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import migsim # noqa: E402 + +g = migsim.g +DAG_ID = "daily_revenue" +RUN_AFTERS = ["2026-03-05T04:15:00+00:00", "2026-03-06T04:15:00+00:00"] +EXPECTED_DEPS = [["load_daily_revenue", "publish_manifest"]] +PLANTED_NAME = "zz_planted_broken.py" +PLANTED_DAG = '''"""Planted by the grader: a DAG file whose import fails.""" +import pendulum +from airflow.sdk import DAG +import finance_helpers_that_do_not_exist # noqa: F401 + +with DAG(dag_id="planted_broken", schedule=None, start_date=pendulum.datetime(2026, 1, 1, tz="UTC")): + pass +''' + + +def normalise(snap: dict[str, str]) -> dict[str, str]: + """Manifests: only ``business_date`` is contractual (the timestamp format may change).""" + out = {} + for path, text in snap.items(): + if path.startswith("manifests/"): + try: + text = json.dumps({"business_date": json.loads(text).get("business_date")}) + except ValueError: + text = f" {text[:200]}" + out[path] = text + return out + + +def migrated_env(py: str, ws: Path) -> dict: + """Fresh AIRFLOW_HOME with a migrated metadata DB, as the team's CI has. + + The original suite already needs it on 2.11 (``DagBag.get_dag`` queries the + ``dag`` table), so a migrated suite that keeps ``get_dag`` must not fail here + just because the grader skipped ``airflow db migrate``. + """ + env = g.airflow_env(ws, env_py=py) + db = g.ensure_db(py, env) + if not db.ok: + raise SystemExit(f"grader: airflow db migrate failed: {db.tail(20)}") + return env + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + # Ground truth: the untouched Airflow 2 project, run by the 2.11 scheduler logic. + orig_ws, orig, orig_log = migsim.run_original(CASE_DIR, DAG_ID, RUN_AFTERS) + if not migsim.runs_ok(orig): + raise SystemExit(f"case bug: original project failed in 2.11: {migsim.describe(orig)}\n{orig_log}") + expected = normalise(migsim.snapshot(orig_ws)) + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly on Airflow 3.3", + imp["ok"] and not imp["import_errors"] + and {DAG_ID, "region_reference"} <= set(imp["dags"]), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"dags: {sorted(imp['dags'])}") + grader.primary("daily_revenue keeps its tasks and dependency", + dag is not None and dag["deps"] == EXPECTED_DEPS + and set(dag["tasks"]) == {"load_daily_revenue", "publish_manifest"}, + f"tasks={dag and dag['tasks']} deps={dag and dag['deps']}") + + shutil.rmtree(ws / "output", ignore_errors=True) + if dag is not None: + # `airflow dags test ` semantics (manual run), retries forced to 0. + cli, cli_log = migsim.simulate(py, ws, DAG_ID, [migsim.manual("2026-03-05")]) + ref, ref_log = migsim.simulate(py, ws, "region_reference", [migsim.manual("2026-03-05")]) + ok = migsim.runs_ok(cli) and migsim.runs_ok(ref) + grader.primary("dags test succeeds (daily_revenue, region_reference)", ok, + "" if ok else f"daily_revenue: {migsim.describe(cli)}\nregion_reference: " + f"{migsim.describe(ref)}\n{(cli_log if not migsim.runs_ok(cli) else ref_log)[-2500:]}") + + shutil.rmtree(ws / "output", ignore_errors=True) + sim, sim_log = migsim.simulate(py, ws, DAG_ID, RUN_AFTERS, block_orm=True) + grader.primary("scheduled 04:15 runs succeed on 3.3", migsim.runs_ok(sim), + migsim.describe(sim) + ("" if migsim.runs_ok(sim) else "\n" + sim_log[-2500:])) + got = normalise(migsim.snapshot(ws)) + grader.primary("scheduled runs process the same business day and write the same output as 2.11", + got == expected, f"expected {expected}\n got {got}") + else: + grader.primary("dags test succeeds (daily_revenue, region_reference)", False, "DAG missing") + + tests = g.run_pytest(py, ws, ["tests"], env=migrated_env(py, ws)) + grader.primary("project test suite passes on 3.3", tests.status == "passed", tests.summary() + + ("" if tests.status == "passed" else "\n" + tests.output_tail[-1500:])) + # The suite is the team's CI gate: a migrated suite that is skipped, gutted or + # no longer parses the DAG folder passes vacuously. Plant a DAG file that cannot + # be imported and require the suite to fail. + planted_ws = Path(tempfile.mkdtemp(prefix="eval-planted-")) / "ws" + shutil.copytree(ws, planted_ws, ignore=shutil.ignore_patterns("output", ".git", "__pycache__", "*.pyc")) + (planted_ws / "dags" / PLANTED_NAME).write_text(PLANTED_DAG) + planted = g.run_pytest(py, planted_ws, ["tests"], env=migrated_env(py, planted_ws)) + caught = tests.status == "passed" and planted.status in ("failed", "collection_error") + grader.primary("project test suite still fails on a DAG file that does not import", caught, + f"clean: {tests.status}; with planted dags/{PLANTED_NAME}: {planted.summary()}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/migration-revenue-previous-day/migsim.py b/evals/airflow/cases/migration-revenue-previous-day/migsim.py new file mode 100644 index 0000000..ffa3509 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/migsim.py @@ -0,0 +1,260 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +This file is identical in every ``migration-*`` case directory. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + dag = load_dag(P["dag_id"]) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + tt = core_timetable(dag) + RESULT["timetable"] = type(tt).__name__ + for spec in P["runs"]: + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(P["dag_id"], logical) + forgive_deadline_bug(row) + continue + run_after = spec + row = {"run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {P['dag_id']} fires at {run_after} ({type(tt).__name__})" + continue + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(P["dag_id"], info.logical_date) + forgive_deadline_bug(row) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is either an ISO datetime T (the *scheduled* run that fires at T, + with the logical date and data interval the DAG's timetable gives it) or + ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), "dag_id": dag_id, + "runs": list(runs), "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + return "; ".join( + f"{r.get('run_after') or 'manual ' + str(r.get('manual'))}: " + (r.get("error") or f"state={r.get('state')} interval=[{r.get('start')}, {r.get('end')}]" + f" failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log diff --git a/evals/airflow/cases/migration-revenue-previous-day/mutants/data_interval_window/dags/sql/daily_revenue.sql b/evals/airflow/cases/migration-revenue-previous-day/mutants/data_interval_window/dags/sql/daily_revenue.sql new file mode 100644 index 0000000..bc20742 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/mutants/data_interval_window/dags/sql/daily_revenue.sql @@ -0,0 +1,21 @@ +-- Completed-order revenue per region for one UTC business day. +SELECT + region, + COUNT(*) AS orders, + CAST(SUM(amount) AS DOUBLE) AS revenue +FROM read_csv( + '{{ params.orders_path }}', + header = true, + columns = { + 'order_id': 'VARCHAR', + 'order_ts': 'TIMESTAMP', + 'region': 'VARCHAR', + 'amount': 'DECIMAL(12, 2)', + 'status': 'VARCHAR' + } +) +WHERE status = 'completed' + AND order_ts >= TIMESTAMP '{{ data_interval_start.strftime("%Y-%m-%d %H:%M:%S") }}' + AND order_ts < TIMESTAMP '{{ data_interval_end.strftime("%Y-%m-%d %H:%M:%S") }}' +GROUP BY region +ORDER BY region diff --git a/evals/airflow/cases/migration-revenue-previous-day/mutants/naive_cron_trigger/dags/daily_revenue.py b/evals/airflow/cases/migration-revenue-previous-day/mutants/naive_cron_trigger/dags/daily_revenue.py new file mode 100644 index 0000000..e258575 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/mutants/naive_cron_trigger/dags/daily_revenue.py @@ -0,0 +1,71 @@ +"""Daily revenue per region. + +Runs every morning at 04:15 UTC, once the late order events for the previous +business day (UTC) have landed, and writes one CSV per business day plus a +small manifest the finance dashboard polls for. +""" + +from __future__ import annotations + +import csv +from datetime import timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_PATH = PROJECT_ROOT / "data" / "orders.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + +default_args = { + "owner": "finance-data", + "retries": 2, + "retry_delay": timedelta(minutes=10), +} + + +def run_revenue_query(templates_dict, ds, **_): + """Run the rendered query and overwrite the CSV for the business day.""" + rows = duckdb.sql(templates_dict["query"]).fetchall() + out = OUTPUT_DIR / "daily_revenue" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region", "orders", "revenue"]) + for region, orders, revenue in rows: + writer.writerow([region, orders, f"{revenue:.2f}"]) + return str(out) + + +with DAG( + dag_id="daily_revenue", + schedule="15 4 * * *", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args=default_args, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"orders_path": str(ORDERS_PATH), "output_dir": str(OUTPUT_DIR)}, + tags=["finance", "revenue"], +) as dag: + load_daily_revenue = PythonOperator( + task_id="load_daily_revenue", + python_callable=run_revenue_query, + templates_dict={"query": "daily_revenue.sql"}, + templates_exts=[".sql"], + ) + + publish_manifest = BashOperator( + task_id="publish_manifest", + bash_command=( + "mkdir -p {{ params.output_dir }}/manifests && " + "echo '{\"business_date\": \"{{ ds }}\", \"logical_ts\": \"{{ logical_date }}\"}' " + "> {{ params.output_dir }}/manifests/{{ ds }}.json" + ), + ) + + load_daily_revenue >> publish_manifest diff --git a/evals/airflow/cases/migration-revenue-previous-day/mutants/next_ds_left_in_sql/dags/sql/daily_revenue.sql b/evals/airflow/cases/migration-revenue-previous-day/mutants/next_ds_left_in_sql/dags/sql/daily_revenue.sql new file mode 100644 index 0000000..7acfdb5 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/mutants/next_ds_left_in_sql/dags/sql/daily_revenue.sql @@ -0,0 +1,21 @@ +-- Completed-order revenue per region for one UTC business day. +SELECT + region, + COUNT(*) AS orders, + CAST(SUM(amount) AS DOUBLE) AS revenue +FROM read_csv( + '{{ params.orders_path }}', + header = true, + columns = { + 'order_id': 'VARCHAR', + 'order_ts': 'TIMESTAMP', + 'region': 'VARCHAR', + 'amount': 'DECIMAL(12, 2)', + 'status': 'VARCHAR' + } +) +WHERE status = 'completed' + AND order_ts >= TIMESTAMP '{{ ds }} 00:00:00' + AND order_ts < TIMESTAMP '{{ next_ds }} 00:00:00' +GROUP BY region +ORDER BY region diff --git a/evals/airflow/cases/migration-revenue-previous-day/mutants/old_dagbag_test/tests/test_dag_integrity.py b/evals/airflow/cases/migration-revenue-previous-day/mutants/old_dagbag_test/tests/test_dag_integrity.py new file mode 100644 index 0000000..00cc214 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/mutants/old_dagbag_test/tests/test_dag_integrity.py @@ -0,0 +1,24 @@ +"""CI checks: every DAG imports and the revenue pipeline keeps its shape.""" + +from pathlib import Path + +import pytest +from airflow.models import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" + + +@pytest.fixture(scope="session") +def dagbag(): + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {} + + +def test_daily_revenue_structure(dagbag): + dag = dagbag.get_dag("daily_revenue") + assert dag is not None + assert set(dag.task_ids) == {"load_daily_revenue", "publish_manifest"} + assert dag.get_task("publish_manifest").upstream_task_ids == {"load_daily_revenue"} diff --git a/evals/airflow/cases/migration-revenue-previous-day/reference/dags/daily_revenue.py b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/daily_revenue.py new file mode 100644 index 0000000..6e16fd5 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/daily_revenue.py @@ -0,0 +1,74 @@ +"""Daily revenue per region. + +Runs every morning at 04:15 UTC, once the late order events for the previous +business day (UTC) have landed, and writes one CSV per business day plus a +small manifest the finance dashboard polls for. +""" + +from __future__ import annotations + +import csv +from datetime import timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +ORDERS_PATH = PROJECT_ROOT / "data" / "orders.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + +default_args = { + "owner": "finance-data", + "retries": 2, + "retry_delay": timedelta(minutes=10), +} + + +def run_revenue_query(templates_dict, ds, **_): + """Run the rendered query and overwrite the CSV for the business day.""" + rows = duckdb.sql(templates_dict["query"]).fetchall() + out = OUTPUT_DIR / "daily_revenue" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region", "orders", "revenue"]) + for region, orders, revenue in rows: + writer.writerow([region, orders, f"{revenue:.2f}"]) + return str(out) + + +with DAG( + dag_id="daily_revenue", + # Airflow 3 maps a bare cron string to CronTriggerTimetable (logical date = + # the tick). Keep the Airflow 2 data-interval semantics: the 04:15 run on day + # D covers [D-1 04:15, D 04:15) and its ``ds`` is the business day D-1. + schedule=CronDataIntervalTimetable("15 4 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args=default_args, + template_searchpath=[str(Path(__file__).parent / "sql")], + params={"orders_path": str(ORDERS_PATH), "output_dir": str(OUTPUT_DIR)}, + tags=["finance", "revenue"], +) as dag: + load_daily_revenue = PythonOperator( + task_id="load_daily_revenue", + python_callable=run_revenue_query, + templates_dict={"query": "daily_revenue.sql"}, + templates_exts=[".sql"], + ) + + publish_manifest = BashOperator( + task_id="publish_manifest", + bash_command=( + "mkdir -p {{ params.output_dir }}/manifests && " + "echo '{\"business_date\": \"{{ ds }}\", \"logical_ts\": \"{{ logical_date }}\"}' " + "> {{ params.output_dir }}/manifests/{{ ds }}.json" + ), + ) + + load_daily_revenue >> publish_manifest diff --git a/evals/airflow/cases/migration-revenue-previous-day/reference/dags/region_reference.py b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/region_reference.py new file mode 100644 index 0000000..bc3e2a6 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/region_reference.py @@ -0,0 +1,36 @@ +"""Rebuild the region reference file on demand (triggered from the UI).""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def build_region_file(): + with (PROJECT_ROOT / "data" / "orders.csv").open(newline="") as fh: + regions = sorted({row["region"] for row in csv.DictReader(fh)}) + out = PROJECT_ROOT / "output" / "reference" / "regions.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["region"]) + writer.writerows([r] for r in regions) + + +with DAG( + dag_id="region_reference", + schedule=None, + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + tags=["finance", "reference"], +) as dag: + start = EmptyOperator(task_id="start") + write_regions = PythonOperator(task_id="write_regions", python_callable=build_region_file) + start >> write_regions diff --git a/evals/airflow/cases/migration-revenue-previous-day/reference/dags/sql/daily_revenue.sql b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/sql/daily_revenue.sql new file mode 100644 index 0000000..5c95901 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/reference/dags/sql/daily_revenue.sql @@ -0,0 +1,21 @@ +-- Completed-order revenue per region for one UTC business day. +SELECT + region, + COUNT(*) AS orders, + CAST(SUM(amount) AS DOUBLE) AS revenue +FROM read_csv( + '{{ params.orders_path }}', + header = true, + columns = { + 'order_id': 'VARCHAR', + 'order_ts': 'TIMESTAMP', + 'region': 'VARCHAR', + 'amount': 'DECIMAL(12, 2)', + 'status': 'VARCHAR' + } +) +WHERE status = 'completed' + AND order_ts >= TIMESTAMP '{{ ds }} 00:00:00' + AND order_ts < TIMESTAMP '{{ macros.ds_add(ds, 1) }} 00:00:00' +GROUP BY region +ORDER BY region diff --git a/evals/airflow/cases/migration-revenue-previous-day/reference/tests/test_dag_integrity.py b/evals/airflow/cases/migration-revenue-previous-day/reference/tests/test_dag_integrity.py new file mode 100644 index 0000000..fbd2c07 --- /dev/null +++ b/evals/airflow/cases/migration-revenue-previous-day/reference/tests/test_dag_integrity.py @@ -0,0 +1,25 @@ +"""CI checks: every DAG imports and the revenue pipeline keeps its shape.""" + +from pathlib import Path + +import pytest +from airflow.dag_processing.dagbag import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" + + +@pytest.fixture(scope="session") +def dagbag(): + # Airflow 3.3: DagBag moved and no longer takes include_examples + # (examples are off through AIRFLOW__CORE__LOAD_EXAMPLES). + return DagBag(dag_folder=str(DAGS_DIR)) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {} + + +def test_daily_revenue_structure(dagbag): + dag = dagbag.dags["daily_revenue"] + assert set(dag.task_ids) == {"load_daily_revenue", "publish_manifest"} + assert dag.get_task("publish_manifest").upstream_task_ids == {"load_daily_revenue"} diff --git a/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/carrier_scorecard.py b/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/carrier_scorecard.py new file mode 100644 index 0000000..699bcdd --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/carrier_scorecard.py @@ -0,0 +1,64 @@ +"""Carrier on-time scorecard, rebuilt whenever a new shipments batch lands.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SCORECARD_DIR = PROJECT_ROOT / "output" / "scorecard" +SHIPMENTS = Asset(name="shipments", uri="warehouse://shipments/batches") + + +@dag( + schedule=SHIPMENTS, + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-data"}, + tags=["logistics", "reporting"], +) +def carrier_scorecard(): + @task + def build_scorecard() -> dict: + latest: dict[str, dict] = {} + for batch in sorted(BATCH_DIR.glob("batch_*.csv")): + with batch.open(newline="") as fh: + for row in csv.DictReader(fh): + key = (row["updated_at"], row["event_id"]) + seen = latest.get(row["shipment_id"]) + if seen is None or key > (seen["updated_at"], seen["event_id"]): + latest[row["shipment_id"]] = row + + shipments, delivered, on_time = defaultdict(int), defaultdict(int), defaultdict(int) + for row in latest.values(): + carrier = row["carrier"] + shipments[carrier] += 1 + if row["status"] == "delivered": + delivered[carrier] += 1 + on_time[carrier] += row["updated_at"][:10] <= row["promised_date"] + + SCORECARD_DIR.mkdir(parents=True, exist_ok=True) + with (SCORECARD_DIR / "carrier_scorecard.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "shipments", "delivered", "on_time", "on_time_rate"]) + for carrier in sorted(shipments): + d = delivered[carrier] + rate = f"{on_time[carrier] / d:.3f}" if d else "" + writer.writerow([carrier, shipments[carrier], d, on_time[carrier], rate]) + return {"carriers": len(shipments), "shipments": len(latest)} + + @task + def mark_ready(stats: dict) -> None: + (SCORECARD_DIR / "_READY").write_text( + f"carriers={stats['carriers']} shipments={stats['shipments']}\n" + ) + + mark_ready(build_scorecard()) + + +carrier_scorecard() diff --git a/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/ingest_shipments.py new file mode 100644 index 0000000..484ebf5 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/alt_reference/dags/ingest_shipments.py @@ -0,0 +1,60 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event received after the end of +the previous successful run's interval, up to the end of this run's interval. +Publishing the batch updates the ``shipments`` asset, which triggers +``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, get_current_context, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Asset(name="shipments", uri="warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] +TS_FORMAT = "%Y-%m-%d %H:%M:%S" + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch() -> int: + ctx = get_current_context() + end = ctx["data_interval_end"] + # The previous successful run loaded everything up to its interval end. + # Airflow 3 exposes it in the context, no metadata-DB query needed. + since = ctx.get("prev_data_interval_end_success") + upper = end.strftime(TS_FORMAT) + lower = since.strftime(TS_FORMAT) if since else None + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (lower is None or r["updated_at"] > lower) + ] + out = BATCH_DIR / f"batch_{end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-shipments-assets/case.yaml b/evals/airflow/cases/migration-shipments-assets/case.yaml new file mode 100644 index 0000000..3a81632 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/case.yaml @@ -0,0 +1,29 @@ +id: migration-shipments-assets +area: migration +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + We need the logistics DAGs in this repo running on Airflow 3.3 (the local + venv is already 3.3.2, production switches over on Monday). Please do the + migration. Ops reconciles the shipment batches against carrier invoices, so + every event still has to be loaded exactly once, and the scorecard and its + _READY file have to keep coming out the same. +description: | + Airflow 2.11 -> 3.3 migration of a producer/consumer project. Pain point P7 + (migration beyond ruff: DB access, datasets, XCom semantics). The hourly + loader keeps its high-water mark in XCom but reads it back with a + `@provide_session` metadata-DB query (blocked for task code in Airflow 3; + ruff is silent). The obvious SDK replacement, + `ti.xcom_pull(..., include_prior_dates=True)`, returns a list on 3.x once + more than one prior value exists, so it only breaks on the third run. + `airflow.datasets.Dataset` no longer imports on 3.3 (ruff reports it only as + AIR311 "suggested update"), and the consumer's BashOperator template uses + `ti.xcom_pull(key=...)` without task_ids, which renders None on 3.x. Primary: + both DAGs import on 3.3, task graphs preserved, consumer still scheduled on + the producer's asset, dags test succeeds under the Airflow 3 worker DB block, + three consecutive hourly loads replayed in 3.3 write the same batches as the + original replayed in 2.11 (no duplicates or gaps), and the scorecard and + _READY marker match. Secondary: verification commands, ruff AIR clean, no + deprecation warnings. diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/.gitignore b/evals/airflow/cases/migration-shipments-assets/fixture/.gitignore new file mode 100644 index 0000000..fd6aad7 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/.gitignore @@ -0,0 +1,2 @@ +output/ +__pycache__/ diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/README.md b/evals/airflow/cases/migration-shipments-assets/fixture/README.md new file mode 100644 index 0000000..7b2e3c5 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/README.md @@ -0,0 +1,10 @@ +# logistics-pipelines + +| DAG | Schedule | What it does | +|---|---|---| +| `ingest_shipments` | hourly | Appends every carrier event received since the last load to `output/warehouse/shipments/batch_.csv`, then updates the `shipments` dataset | +| `carrier_scorecard` | on `shipments` updates | Rebuilds `output/scorecard/carrier_scorecard.csv` from all batches and writes `output/scorecard/_READY` for the ops dashboard | + +`data/carrier_events.csv` stands in for the carrier API export (timestamps are +UTC). Every event must be loaded exactly once: the ops team reconciles batch +row counts against the carrier's invoices. diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/dags/carrier_scorecard.py b/evals/airflow/cases/migration-shipments-assets/fixture/dags/carrier_scorecard.py new file mode 100644 index 0000000..2d4f784 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/dags/carrier_scorecard.py @@ -0,0 +1,69 @@ +"""Carrier on-time scorecard, rebuilt whenever a new shipments batch lands.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow import DAG +from airflow.datasets import Dataset +from airflow.operators.bash import BashOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SCORECARD_DIR = PROJECT_ROOT / "output" / "scorecard" +SHIPMENTS = Dataset("warehouse://shipments/batches") + + +def build_scorecard(ti, **_): + latest: dict[str, dict] = {} + for batch in sorted(BATCH_DIR.glob("batch_*.csv")): + with batch.open(newline="") as fh: + for row in csv.DictReader(fh): + seen = latest.get(row["shipment_id"]) + if seen is None or (row["updated_at"], row["event_id"]) > (seen["updated_at"], seen["event_id"]): + latest[row["shipment_id"]] = row + + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # shipments, delivered, on time + for row in latest.values(): + s = stats[row["carrier"]] + s[0] += 1 + if row["status"] == "delivered": + s[1] += 1 + if row["updated_at"][:10] <= row["promised_date"]: + s[2] += 1 + + SCORECARD_DIR.mkdir(parents=True, exist_ok=True) + with (SCORECARD_DIR / "carrier_scorecard.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "shipments", "delivered", "on_time", "on_time_rate"]) + for carrier in sorted(stats): + shipments, delivered, on_time = stats[carrier] + rate = f"{on_time / delivered:.3f}" if delivered else "" + writer.writerow([carrier, shipments, delivered, on_time, rate]) + ti.xcom_push(key="carriers", value=len(stats)) + ti.xcom_push(key="shipments", value=len(latest)) + + +with DAG( + dag_id="carrier_scorecard", + schedule=[SHIPMENTS], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-data"}, + params={"scorecard_dir": str(SCORECARD_DIR)}, + tags=["logistics", "reporting"], +) as dag: + build = PythonOperator(task_id="build_scorecard", python_callable=build_scorecard) + mark_ready = BashOperator( + task_id="mark_ready", + bash_command=( + "echo 'carriers={{ ti.xcom_pull(key=\"carriers\") }} " + "shipments={{ ti.xcom_pull(key=\"shipments\") }}' " + "> {{ params.scorecard_dir }}/_READY" + ), + ) + build >> mark_ready diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/fixture/dags/ingest_shipments.py new file mode 100644 index 0000000..d5050e6 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/dags/ingest_shipments.py @@ -0,0 +1,74 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event that arrived since the +previous run's high-water mark (the largest ``updated_at`` it loaded, kept in +XCom) up to the end of the run's interval. Publishing the batch updates the +``shipments`` dataset, which triggers ``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.datasets import Dataset +from airflow.decorators import dag, task +from airflow.models import XCom +from airflow.utils.session import provide_session + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Dataset("warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] + + +@provide_session +def previous_watermark(session=None) -> str | None: + """High-water mark stored by the most recent load.""" + row = ( + session.query(XCom) + .filter( + XCom.dag_id == "ingest_shipments", + XCom.task_id == "load_batch", + XCom.key == "watermark", + ) + .order_by(XCom.timestamp.desc()) + .first() + ) + return row.value if row else None + + +@dag( + schedule_interval="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch(data_interval_end=None, ti=None) -> int: + watermark = previous_watermark() + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (watermark is None or r["updated_at"] > watermark) + ] + out = BATCH_DIR / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="watermark", value=max((r["updated_at"] for r in rows), default=watermark)) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/data/carrier_events.csv b/evals/airflow/cases/migration-shipments-assets/fixture/data/carrier_events.csv new file mode 100644 index 0000000..acca8d1 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/data/carrier_events.csv @@ -0,0 +1,154 @@ +event_id,shipment_id,carrier,status,promised_date,updated_at +E00007,SH-7002,USPX,picked_up,2026-03-04,2026-03-04 18:06:00 +E00042,SH-7017,FEDX,picked_up,2026-03-04,2026-03-04 18:06:00 +E00105,SH-7041,UPSX,picked_up,2026-03-07,2026-03-04 18:06:00 +E00001,SH-7000,USPX,picked_up,2026-03-05,2026-03-04 18:07:00 +E00065,SH-7027,DHLX,picked_up,2026-03-07,2026-03-04 18:10:00 +E00099,SH-7039,USPX,picked_up,2026-03-07,2026-03-04 18:13:00 +E00143,SH-7055,USPX,picked_up,2026-03-07,2026-03-04 18:19:00 +E00046,SH-7019,FEDX,picked_up,2026-03-06,2026-03-04 18:26:00 +E00106,SH-7041,UPSX,in_transit,2026-03-07,2026-03-04 18:31:00 +E00051,SH-7021,FEDX,picked_up,2026-03-05,2026-03-04 18:35:00 +E00035,SH-7014,UPSX,picked_up,2026-03-05,2026-03-04 18:41:00 +E00108,SH-7042,USPX,picked_up,2026-03-04,2026-03-04 18:45:00 +E00034,SH-7013,USPX,picked_up,2026-03-07,2026-03-04 18:55:00 +E00124,SH-7048,FEDX,picked_up,2026-03-06,2026-03-04 18:55:00 +E00135,SH-7052,DHLX,picked_up,2026-03-07,2026-03-04 18:56:00 +E00008,SH-7002,USPX,in_transit,2026-03-04,2026-03-04 19:02:00 +E00107,SH-7041,UPSX,delivered,2026-03-07,2026-03-04 19:05:00 +E00018,SH-7006,UPSX,picked_up,2026-03-07,2026-03-04 19:08:00 +E00048,SH-7020,DHLX,picked_up,2026-03-06,2026-03-04 19:09:00 +E00150,SH-7059,FEDX,picked_up,2026-03-07,2026-03-04 19:10:00 +E00125,SH-7048,FEDX,in_transit,2026-03-06,2026-03-04 19:16:00 +E00068,SH-7028,FEDX,picked_up,2026-03-05,2026-03-04 19:22:00 +E00013,SH-7004,DHLX,picked_up,2026-03-06,2026-03-04 19:26:00 +E00096,SH-7038,USPX,picked_up,2026-03-04,2026-03-04 19:26:00 +E00069,SH-7029,FEDX,picked_up,2026-03-05,2026-03-04 19:31:00 +E00147,SH-7058,UPSX,picked_up,2026-03-04,2026-03-04 19:31:00 +E00052,SH-7021,FEDX,in_transit,2026-03-05,2026-03-04 19:34:00 +E00039,SH-7016,FEDX,picked_up,2026-03-07,2026-03-04 19:35:00 +E00016,SH-7005,UPSX,picked_up,2026-03-07,2026-03-04 19:43:00 +E00009,SH-7002,USPX,delivered,2026-03-04,2026-03-04 19:48:00 +E00075,SH-7031,USPX,picked_up,2026-03-04,2026-03-04 19:57:00 +E00140,SH-7054,FEDX,picked_up,2026-03-06,2026-03-04 19:59:00 +E00021,SH-7007,FEDX,picked_up,2026-03-06,2026-03-04 20:03:00 +E00127,SH-7049,FEDX,picked_up,2026-03-07,2026-03-04 20:03:00 +E00002,SH-7000,USPX,in_transit,2026-03-05,2026-03-04 20:04:00 +E00024,SH-7009,USPX,picked_up,2026-03-05,2026-03-04 20:06:00 +E00100,SH-7039,USPX,in_transit,2026-03-07,2026-03-04 20:08:00 +E00043,SH-7018,FEDX,picked_up,2026-03-04,2026-03-04 20:11:00 +E00114,SH-7044,DHLX,picked_up,2026-03-04,2026-03-04 20:14:00 +E00084,SH-7034,FEDX,picked_up,2026-03-06,2026-03-04 20:15:00 +E00030,SH-7011,UPSX,picked_up,2026-03-04,2026-03-04 20:16:00 +E00121,SH-7047,DHLX,picked_up,2026-03-05,2026-03-04 20:18:00 +E00126,SH-7048,FEDX,delivered,2026-03-06,2026-03-04 20:19:00 +E00040,SH-7016,FEDX,in_transit,2026-03-07,2026-03-04 20:23:00 +E00081,SH-7033,DHLX,picked_up,2026-03-06,2026-03-04 20:24:00 +E00148,SH-7058,UPSX,in_transit,2026-03-04,2026-03-04 20:25:00 +E00066,SH-7027,DHLX,in_transit,2026-03-07,2026-03-04 20:28:00 +E00014,SH-7004,DHLX,in_transit,2026-03-06,2026-03-04 20:29:00 +E00132,SH-7051,DHLX,picked_up,2026-03-04,2026-03-04 20:34:00 +E00037,SH-7015,DHLX,picked_up,2026-03-07,2026-03-04 20:43:00 +E00047,SH-7019,FEDX,in_transit,2026-03-06,2026-03-04 20:43:00 +E00022,SH-7007,FEDX,in_transit,2026-03-06,2026-03-04 20:44:00 +E00136,SH-7052,DHLX,in_transit,2026-03-07,2026-03-04 20:45:00 +E00078,SH-7032,DHLX,picked_up,2026-03-05,2026-03-04 20:47:00 +E00093,SH-7037,UPSX,picked_up,2026-03-07,2026-03-04 20:47:00 +E00146,SH-7057,USPX,picked_up,2026-03-07,2026-03-04 20:47:00 +E00036,SH-7014,UPSX,in_transit,2026-03-05,2026-03-04 20:49:00 +E00023,SH-7008,USPX,picked_up,2026-03-07,2026-03-04 20:50:00 +E00097,SH-7038,USPX,in_transit,2026-03-04,2026-03-04 20:50:00 +E00076,SH-7031,USPX,in_transit,2026-03-04,2026-03-04 20:52:00 +E00090,SH-7036,UPSX,picked_up,2026-03-05,2026-03-04 20:53:00 +E00049,SH-7020,DHLX,in_transit,2026-03-06,2026-03-04 20:59:00 +E00061,SH-7025,UPSX,picked_up,2026-03-07,2026-03-04 21:00:00 +E00070,SH-7029,FEDX,in_transit,2026-03-05,2026-03-04 21:04:00 +E00003,SH-7000,USPX,delivered,2026-03-05,2026-03-04 21:06:00 +E00072,SH-7030,FEDX,picked_up,2026-03-05,2026-03-04 21:08:00 +E00054,SH-7022,DHLX,picked_up,2026-03-04,2026-03-04 21:11:00 +E00109,SH-7042,USPX,in_transit,2026-03-04,2026-03-04 21:19:00 +E00017,SH-7005,UPSX,in_transit,2026-03-07,2026-03-04 21:21:00 +E00137,SH-7053,UPSX,picked_up,2026-03-07,2026-03-04 21:21:00 +E00033,SH-7012,USPX,picked_up,2026-03-05,2026-03-04 21:23:00 +E00044,SH-7018,FEDX,in_transit,2026-03-04,2026-03-04 21:24:00 +E00053,SH-7021,FEDX,delivered,2026-03-05,2026-03-04 21:24:00 +E00067,SH-7027,DHLX,delivered,2026-03-07,2026-03-04 21:25:00 +E00077,SH-7031,USPX,delivered,2026-03-04,2026-03-04 21:25:00 +E00004,SH-7001,USPX,picked_up,2026-03-04,2026-03-04 21:28:00 +E00019,SH-7006,UPSX,in_transit,2026-03-07,2026-03-04 21:30:00 +E00115,SH-7045,USPX,picked_up,2026-03-04,2026-03-04 21:32:00 +E00057,SH-7023,FEDX,picked_up,2026-03-05,2026-03-04 21:33:00 +E00144,SH-7055,USPX,in_transit,2026-03-07,2026-03-04 21:35:00 +E00087,SH-7035,DHLX,picked_up,2026-03-04,2026-03-04 21:37:00 +E00059,SH-7024,USPX,picked_up,2026-03-05,2026-03-04 21:38:00 +E00141,SH-7054,FEDX,in_transit,2026-03-06,2026-03-04 21:39:00 +E00122,SH-7047,DHLX,in_transit,2026-03-05,2026-03-04 21:40:00 +E00010,SH-7003,FEDX,picked_up,2026-03-05,2026-03-04 21:41:00 +E00063,SH-7026,USPX,picked_up,2026-03-05,2026-03-04 21:41:00 +E00111,SH-7043,FEDX,picked_up,2026-03-04,2026-03-04 21:41:00 +E00129,SH-7050,UPSX,picked_up,2026-03-04,2026-03-04 21:41:00 +E00118,SH-7046,USPX,picked_up,2026-03-05,2026-03-04 21:42:00 +E00027,SH-7010,USPX,picked_up,2026-03-05,2026-03-04 21:43:00 +E00128,SH-7049,FEDX,in_transit,2026-03-07,2026-03-04 21:47:00 +E00102,SH-7040,FEDX,picked_up,2026-03-05,2026-03-04 21:48:00 +E00145,SH-7056,DHLX,picked_up,2026-03-05,2026-03-04 21:56:00 +E00079,SH-7032,DHLX,in_transit,2026-03-05,2026-03-04 22:04:00 +E00130,SH-7050,UPSX,in_transit,2026-03-04,2026-03-04 22:04:00 +E00138,SH-7053,UPSX,in_transit,2026-03-07,2026-03-04 22:09:00 +E00055,SH-7022,DHLX,in_transit,2026-03-04,2026-03-04 22:18:00 +E00133,SH-7051,DHLX,in_transit,2026-03-04,2026-03-04 22:23:00 +E00031,SH-7011,UPSX,in_transit,2026-03-04,2026-03-04 22:24:00 +E00060,SH-7024,USPX,in_transit,2026-03-05,2026-03-04 22:26:00 +E00005,SH-7001,USPX,in_transit,2026-03-04,2026-03-04 22:27:00 +E00085,SH-7034,FEDX,in_transit,2026-03-06,2026-03-04 22:28:00 +E00091,SH-7036,UPSX,in_transit,2026-03-05,2026-03-04 22:32:00 +E00062,SH-7025,UPSX,in_transit,2026-03-07,2026-03-04 22:38:00 +E00098,SH-7038,USPX,delivered,2026-03-04,2026-03-04 22:39:00 +E00073,SH-7030,FEDX,in_transit,2026-03-05,2026-03-04 22:44:00 +E00123,SH-7047,DHLX,delivered,2026-03-05,2026-03-04 22:44:00 +E00032,SH-7011,UPSX,delivered,2026-03-04,2026-03-04 22:45:00 +E00149,SH-7058,UPSX,delivered,2026-03-04,2026-03-04 22:47:00 +E00088,SH-7035,DHLX,in_transit,2026-03-04,2026-03-04 22:48:00 +E00082,SH-7033,DHLX,in_transit,2026-03-06,2026-03-04 22:50:00 +E00028,SH-7010,USPX,in_transit,2026-03-05,2026-03-04 22:58:00 +E00025,SH-7009,USPX,in_transit,2026-03-05,2026-03-04 23:04:00 +E00064,SH-7026,USPX,in_transit,2026-03-05,2026-03-04 23:05:00 +E00038,SH-7015,DHLX,in_transit,2026-03-07,2026-03-04 23:07:00 +E00094,SH-7037,UPSX,in_transit,2026-03-07,2026-03-04 23:07:00 +E00112,SH-7043,FEDX,in_transit,2026-03-04,2026-03-04 23:10:00 +E00045,SH-7018,FEDX,delivered,2026-03-04,2026-03-04 23:13:00 +E00119,SH-7046,USPX,in_transit,2026-03-05,2026-03-04 23:13:00 +E00056,SH-7022,DHLX,delivered,2026-03-04,2026-03-04 23:24:00 +E00029,SH-7010,USPX,delivered,2026-03-05,2026-03-04 23:34:00 +E00041,SH-7016,FEDX,delivered,2026-03-07,2026-03-04 23:35:00 +E00015,SH-7004,DHLX,delivered,2026-03-06,2026-03-04 23:36:00 +E00074,SH-7030,FEDX,delivered,2026-03-05,2026-03-04 23:44:00 +E00092,SH-7036,UPSX,delivered,2026-03-05,2026-03-04 23:48:00 +E00101,SH-7039,USPX,delivered,2026-03-07,2026-03-05 00:03:00 +E00050,SH-7020,DHLX,delivered,2026-03-06,2026-03-05 00:05:00 +E00089,SH-7035,DHLX,delivered,2026-03-04,2026-03-05 00:09:00 +E00116,SH-7045,USPX,in_transit,2026-03-04,2026-03-05 00:09:00 +E00071,SH-7029,FEDX,delivered,2026-03-05,2026-03-05 00:12:00 +E00131,SH-7050,UPSX,delivered,2026-03-04,2026-03-05 00:14:00 +E00058,SH-7023,FEDX,in_transit,2026-03-05,2026-03-05 00:18:00 +E00020,SH-7006,UPSX,delivered,2026-03-07,2026-03-05 00:21:00 +E00110,SH-7042,USPX,delivered,2026-03-04,2026-03-05 00:29:00 +E00142,SH-7054,FEDX,delivered,2026-03-06,2026-03-05 00:40:00 +E00086,SH-7034,FEDX,delivered,2026-03-06,2026-03-05 00:41:00 +E00083,SH-7033,DHLX,delivered,2026-03-06,2026-03-05 00:46:00 +E00103,SH-7040,FEDX,in_transit,2026-03-05,2026-03-05 00:50:00 +E00011,SH-7003,FEDX,in_transit,2026-03-05,2026-03-05 00:52:00 +E00151,SH-7101,FEDX,picked_up,2026-03-05,2026-03-05 01:00:00 +E00117,SH-7045,USPX,delivered,2026-03-04,2026-03-05 01:20:00 +E00006,SH-7001,USPX,delivered,2026-03-04,2026-03-05 01:34:00 +E00026,SH-7009,USPX,delivered,2026-03-05,2026-03-05 01:38:00 +E00139,SH-7053,UPSX,delivered,2026-03-07,2026-03-05 01:53:00 +E00152,SH-7102,UPSX,picked_up,2026-03-05,2026-03-05 02:00:00 +E00080,SH-7032,DHLX,delivered,2026-03-05,2026-03-05 02:03:00 +E00134,SH-7051,DHLX,delivered,2026-03-04,2026-03-05 02:05:00 +E00113,SH-7043,FEDX,delivered,2026-03-04,2026-03-05 02:08:00 +E00120,SH-7046,USPX,delivered,2026-03-05,2026-03-05 02:13:00 +E00095,SH-7037,UPSX,delivered,2026-03-07,2026-03-05 02:52:00 +E00153,SH-7103,USPX,picked_up,2026-03-05,2026-03-05 03:00:00 +E00012,SH-7003,FEDX,delivered,2026-03-05,2026-03-05 03:01:00 +E00104,SH-7040,FEDX,delivered,2026-03-05,2026-03-05 03:29:00 diff --git a/evals/airflow/cases/migration-shipments-assets/fixture/requirements.txt b/evals/airflow/cases/migration-shipments-assets/fixture/requirements.txt new file mode 100644 index 0000000..7056ae9 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==2.11.2 +pendulum==3.2.0 diff --git a/evals/airflow/cases/migration-shipments-assets/grade.py b/evals/airflow/cases/migration-shipments-assets/grade.py new file mode 100644 index 0000000..bf6abd9 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/grade.py @@ -0,0 +1,147 @@ +"""Grader for migration-shipments-assets. + +The fixture is an Airflow 2.11 logistics project: an hourly incremental load +(``ingest_shipments``) that publishes a Dataset, and a Dataset-scheduled +consumer (``carrier_scorecard``). Airflow 3 breaks it in ways ruff only partly +sees: ``airflow.datasets`` is gone (ruff reports it, but only as a +"suggested update"), the loader reads its high-water mark with a metadata-DB +session (blocked for task code in Airflow 3; the natural XCom replacement with +``include_prior_dates=True`` returns a LIST once several runs exist), and the +consumer's Jinja ``ti.xcom_pull(key=...)`` without ``task_ids`` renders +``None`` on 3.x. + +Behaviour is compared with the ORIGINAL project replayed in the 2.11 env: three +consecutive hourly runs of the loader against one metadata DB, then a run of +the scorecard. Task code runs with the Airflow 3 worker's metadata-DB block +emulated (``migsim``). +""" + +from __future__ import annotations + +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import migsim # noqa: E402 + +g = migsim.g +PRODUCER = "ingest_shipments" +CONSUMER = "carrier_scorecard" +RUN_AFTERS = ["2026-03-05T01:00:00+00:00", "2026-03-05T02:00:00+00:00", "2026-03-05T03:00:00+00:00"] +CONSUMER_RUN = migsim.manual("2026-03-05T03:30:00+00:00") +EXPECTED_TASKS = {PRODUCER: ({"load_batch"}, []), + CONSUMER: ({"build_scorecard", "mark_ready"}, [["build_scorecard", "mark_ready"]])} + +WIRING_PROBE = f""" +def keys(obj): + # Asset -> uri/name keys; conditions (AssetAll/AssetAny) -> their members; refs -> name/uri. + if hasattr(obj, "objects"): + out = set() + for o in obj.objects: + out |= keys(o) + return out + out = set() + if getattr(obj, "uri", None): + out.add("uri:" + obj.uri) + if getattr(obj, "name", None): + out.add("name:" + obj.name) + return out or {{"repr:" + repr(obj)}} + +prod = get_dag({PRODUCER!r}) +cons = get_dag({CONSUMER!r}) +outlets = set() +for t in prod.tasks: + for a in (t.outlets or []): + outlets |= keys(a) +tt = cons.timetable +cond = getattr(tt, "asset_condition", None) +scheduled = keys(cond) if cond is not None else set() +RESULT = {{"outlets": sorted(outlets), "schedule_assets": sorted(scheduled), "timetable": type(tt).__name__}} +""" + + +def run_flow(py: str, ws: Path, block_orm: bool) -> tuple[dict, dict, str]: + """Three hourly loads, then the scorecard, against one metadata DB.""" + env = g.airflow_env(ws, env_py=py) + prod, prod_log = migsim.simulate(py, ws, PRODUCER, RUN_AFTERS, env=env, block_orm=block_orm) + cons, cons_log = migsim.simulate(py, ws, CONSUMER, [CONSUMER_RUN], env=env, block_orm=block_orm) + return prod, cons, (prod_log if not migsim.runs_ok(prod) else cons_log) + + +def split(snap: dict[str, str]) -> tuple[dict[str, str], dict[str, str]]: + batches = {k: v for k, v in snap.items() if k.startswith("warehouse/")} + reports = {k: v for k, v in snap.items() if k.startswith("scorecard/")} + return batches, reports + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + orig_ws = migsim.original_workspace(CASE_DIR) + prod, cons, log = run_flow(g.env_python("2.11"), orig_ws, block_orm=False) + if not (migsim.runs_ok(prod) and migsim.runs_ok(cons)): + raise SystemExit(f"case bug: original failed in 2.11: {migsim.describe(prod)} | " + f"{migsim.describe(cons)}\n{log}") + exp_batches, exp_reports = split(migsim.snapshot(orig_ws)) + if len(exp_batches) != len(RUN_AFTERS) or "scorecard/_READY" not in exp_reports: + raise SystemExit(f"case bug: unexpected original output {sorted(exp_batches)} {sorted(exp_reports)}") + + imp = g.import_dags(py, ws) + dags = imp["dags"] + grader.primary("DAGs import cleanly on Airflow 3.3", + imp["ok"] and not imp["import_errors"] and {PRODUCER, CONSUMER} <= set(dags), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"dags: {sorted(dags)}") + shape_ok = all(d in dags and set(dags[d]["tasks"]) == tasks and dags[d]["deps"] == deps + for d, (tasks, deps) in EXPECTED_TASKS.items()) + grader.primary("both DAGs keep their tasks and dependencies", shape_ok, + "; ".join(f"{d}: tasks={dags[d]['tasks']} deps={dags[d]['deps']}" if d in dags else f"{d}: missing" + for d in EXPECTED_TASKS)) + + if not {PRODUCER, CONSUMER} <= set(dags): + grader.primary("carrier_scorecard is still scheduled on the asset ingest_shipments updates", False, + "DAG missing") + grader.primary("dags test succeeds (ingest_shipments, carrier_scorecard)", False, "DAG missing") + else: + wiring, proc = g.probe_json(py, WIRING_PROBE, ws, env=g.airflow_env(ws, env_py=py)) + wired = bool(wiring) and bool(set(wiring["outlets"]) & set(wiring["schedule_assets"])) + grader.primary("carrier_scorecard is still scheduled on the asset ingest_shipments updates", wired, + str(wiring) if wiring is not None else proc.tail(20)) + + shutil.rmtree(ws / "output", ignore_errors=True) + p_cli, p_log = migsim.simulate(py, ws, PRODUCER, [migsim.manual("2026-03-05T01:00:00+00:00")], + block_orm=True) + c_cli, c_log = migsim.simulate(py, ws, CONSUMER, [migsim.manual("2026-03-05T01:30:00+00:00")], + block_orm=True) + ok = migsim.runs_ok(p_cli) and migsim.runs_ok(c_cli) + grader.primary("dags test succeeds (ingest_shipments, carrier_scorecard)", ok, + "" if ok else f"{PRODUCER}: {migsim.describe(p_cli)}\n{CONSUMER}: {migsim.describe(c_cli)}\n" + f"{(p_log if not migsim.runs_ok(p_cli) else c_log)[-2500:]}") + + shutil.rmtree(ws / "output", ignore_errors=True) + prod, cons, log = run_flow(py, ws, block_orm=True) + runs_ok = migsim.runs_ok(prod) and migsim.runs_ok(cons) + grader.primary("three hourly loads and the scorecard run succeed on 3.3", runs_ok, + f"{PRODUCER}: {migsim.describe(prod)}\n{CONSUMER}: {migsim.describe(cons)}" + + ("" if runs_ok else "\n" + log[-2500:])) + got_batches, got_reports = split(migsim.snapshot(ws)) + grader.primary("every carrier event is loaded exactly once, in the same batches as 2.11", + got_batches == exp_batches, + f"expected batches {sorted(exp_batches)} " + f"(rows {[len(v.splitlines()) - 1 for v in exp_batches.values()]})\n" + f" got {sorted(got_batches)} (rows {[len(v.splitlines()) - 1 for v in got_batches.values()]})") + grader.primary("scorecard and _READY marker match 2.11", got_reports == exp_reports, + f"expected {exp_reports}\n got {got_reports}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/migration-shipments-assets/migsim.py b/evals/airflow/cases/migration-shipments-assets/migsim.py new file mode 100644 index 0000000..ffa3509 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/migsim.py @@ -0,0 +1,260 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +This file is identical in every ``migration-*`` case directory. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + dag = load_dag(P["dag_id"]) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + tt = core_timetable(dag) + RESULT["timetable"] = type(tt).__name__ + for spec in P["runs"]: + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(P["dag_id"], logical) + forgive_deadline_bug(row) + continue + run_after = spec + row = {"run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {P['dag_id']} fires at {run_after} ({type(tt).__name__})" + continue + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(P["dag_id"], info.logical_date) + forgive_deadline_bug(row) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is either an ISO datetime T (the *scheduled* run that fires at T, + with the logical date and data interval the DAG's timetable gives it) or + ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), "dag_id": dag_id, + "runs": list(runs), "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + return "; ".join( + f"{r.get('run_after') or 'manual ' + str(r.get('manual'))}: " + (r.get("error") or f"state={r.get('state')} interval=[{r.get('start')}, {r.get('end')}]" + f" failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log diff --git a/evals/airflow/cases/migration-shipments-assets/mutants/db_session_kept/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/mutants/db_session_kept/dags/ingest_shipments.py new file mode 100644 index 0000000..4b4f7f5 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/mutants/db_session_kept/dags/ingest_shipments.py @@ -0,0 +1,73 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event that arrived since the +previous run's high-water mark (the largest ``updated_at`` it loaded, kept in +XCom) up to the end of the run's interval. Publishing the batch updates the +``shipments`` asset, which triggers ``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, task +from airflow.models import XCom +from airflow.utils.session import provide_session + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Asset("warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] + + +@provide_session +def previous_watermark(session=None) -> str | None: + """High-water mark stored by the most recent load.""" + row = ( + session.query(XCom) + .filter( + XCom.dag_id == "ingest_shipments", + XCom.task_id == "load_batch", + XCom.key == "watermark", + ) + .order_by(XCom.timestamp.desc()) + .first() + ) + return row.value if row else None + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch(data_interval_end=None, ti=None) -> int: + watermark = previous_watermark() + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (watermark is None or r["updated_at"] > watermark) + ] + out = BATCH_DIR / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="watermark", value=max((r["updated_at"] for r in rows), default=watermark)) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-shipments-assets/mutants/watermark_current_run_only/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/mutants/watermark_current_run_only/dags/ingest_shipments.py new file mode 100644 index 0000000..ae335e7 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/mutants/watermark_current_run_only/dags/ingest_shipments.py @@ -0,0 +1,56 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event that arrived since the +previous run's high-water mark (the largest ``updated_at`` it loaded, kept in +XCom) up to the end of the run's interval. Publishing the batch updates the +``shipments`` asset, which triggers ``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Asset("warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch(data_interval_end=None, ti=None) -> int: + # Airflow 3 blocks metadata-DB access: read the previous watermark via XCom. + watermark = ti.xcom_pull(task_ids="load_batch", key="watermark") + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (watermark is None or r["updated_at"] > watermark) + ] + out = BATCH_DIR / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="watermark", value=max((r["updated_at"] for r in rows), default=watermark)) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-shipments-assets/mutants/xcom_prior_dates_list/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/mutants/xcom_prior_dates_list/dags/ingest_shipments.py new file mode 100644 index 0000000..a3d62fa --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/mutants/xcom_prior_dates_list/dags/ingest_shipments.py @@ -0,0 +1,56 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event that arrived since the +previous run's high-water mark (the largest ``updated_at`` it loaded, kept in +XCom) up to the end of the run's interval. Publishing the batch updates the +``shipments`` asset, which triggers ``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Asset("warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch(data_interval_end=None, ti=None) -> int: + # Airflow 3 blocks metadata-DB access: read the previous watermark via XCom. + watermark = ti.xcom_pull(task_ids="load_batch", key="watermark", include_prior_dates=True) + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (watermark is None or r["updated_at"] > watermark) + ] + out = BATCH_DIR / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="watermark", value=max((r["updated_at"] for r in rows), default=watermark)) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-shipments-assets/mutants/xcom_pull_template_no_task_ids/dags/carrier_scorecard.py b/evals/airflow/cases/migration-shipments-assets/mutants/xcom_pull_template_no_task_ids/dags/carrier_scorecard.py new file mode 100644 index 0000000..9646e93 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/mutants/xcom_pull_template_no_task_ids/dags/carrier_scorecard.py @@ -0,0 +1,68 @@ +"""Carrier on-time scorecard, rebuilt whenever a new shipments batch lands.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, Asset + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SCORECARD_DIR = PROJECT_ROOT / "output" / "scorecard" +SHIPMENTS = Asset("warehouse://shipments/batches") + + +def build_scorecard(ti, **_): + latest: dict[str, dict] = {} + for batch in sorted(BATCH_DIR.glob("batch_*.csv")): + with batch.open(newline="") as fh: + for row in csv.DictReader(fh): + seen = latest.get(row["shipment_id"]) + if seen is None or (row["updated_at"], row["event_id"]) > (seen["updated_at"], seen["event_id"]): + latest[row["shipment_id"]] = row + + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # shipments, delivered, on time + for row in latest.values(): + s = stats[row["carrier"]] + s[0] += 1 + if row["status"] == "delivered": + s[1] += 1 + if row["updated_at"][:10] <= row["promised_date"]: + s[2] += 1 + + SCORECARD_DIR.mkdir(parents=True, exist_ok=True) + with (SCORECARD_DIR / "carrier_scorecard.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "shipments", "delivered", "on_time", "on_time_rate"]) + for carrier in sorted(stats): + shipments, delivered, on_time = stats[carrier] + rate = f"{on_time / delivered:.3f}" if delivered else "" + writer.writerow([carrier, shipments, delivered, on_time, rate]) + ti.xcom_push(key="carriers", value=len(stats)) + ti.xcom_push(key="shipments", value=len(latest)) + + +with DAG( + dag_id="carrier_scorecard", + schedule=[SHIPMENTS], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-data"}, + params={"scorecard_dir": str(SCORECARD_DIR)}, + tags=["logistics", "reporting"], +) as dag: + build = PythonOperator(task_id="build_scorecard", python_callable=build_scorecard) + mark_ready = BashOperator( + task_id="mark_ready", + bash_command=( + "echo 'carriers={{ ti.xcom_pull(key=\"carriers\") }} " + "shipments={{ ti.xcom_pull(key=\"shipments\") }}' " + "> {{ params.scorecard_dir }}/_READY" + ), + ) + build >> mark_ready diff --git a/evals/airflow/cases/migration-shipments-assets/reference/dags/carrier_scorecard.py b/evals/airflow/cases/migration-shipments-assets/reference/dags/carrier_scorecard.py new file mode 100644 index 0000000..03fb4e3 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/reference/dags/carrier_scorecard.py @@ -0,0 +1,68 @@ +"""Carrier on-time scorecard, rebuilt whenever a new shipments batch lands.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.bash import BashOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, Asset + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SCORECARD_DIR = PROJECT_ROOT / "output" / "scorecard" +SHIPMENTS = Asset("warehouse://shipments/batches") + + +def build_scorecard(ti, **_): + latest: dict[str, dict] = {} + for batch in sorted(BATCH_DIR.glob("batch_*.csv")): + with batch.open(newline="") as fh: + for row in csv.DictReader(fh): + seen = latest.get(row["shipment_id"]) + if seen is None or (row["updated_at"], row["event_id"]) > (seen["updated_at"], seen["event_id"]): + latest[row["shipment_id"]] = row + + stats: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0]) # shipments, delivered, on time + for row in latest.values(): + s = stats[row["carrier"]] + s[0] += 1 + if row["status"] == "delivered": + s[1] += 1 + if row["updated_at"][:10] <= row["promised_date"]: + s[2] += 1 + + SCORECARD_DIR.mkdir(parents=True, exist_ok=True) + with (SCORECARD_DIR / "carrier_scorecard.csv").open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "shipments", "delivered", "on_time", "on_time_rate"]) + for carrier in sorted(stats): + shipments, delivered, on_time = stats[carrier] + rate = f"{on_time / delivered:.3f}" if delivered else "" + writer.writerow([carrier, shipments, delivered, on_time, rate]) + ti.xcom_push(key="carriers", value=len(stats)) + ti.xcom_push(key="shipments", value=len(latest)) + + +with DAG( + dag_id="carrier_scorecard", + schedule=[SHIPMENTS], + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "logistics-data"}, + params={"scorecard_dir": str(SCORECARD_DIR)}, + tags=["logistics", "reporting"], +) as dag: + build = PythonOperator(task_id="build_scorecard", python_callable=build_scorecard) + mark_ready = BashOperator( + task_id="mark_ready", + bash_command=( + "echo 'carriers={{ ti.xcom_pull(task_ids=\"build_scorecard\", key=\"carriers\") }} " + "shipments={{ ti.xcom_pull(task_ids=\"build_scorecard\", key=\"shipments\") }}' " + "> {{ params.scorecard_dir }}/_READY" + ), + ) + build >> mark_ready diff --git a/evals/airflow/cases/migration-shipments-assets/reference/dags/ingest_shipments.py b/evals/airflow/cases/migration-shipments-assets/reference/dags/ingest_shipments.py new file mode 100644 index 0000000..49d3a09 --- /dev/null +++ b/evals/airflow/cases/migration-shipments-assets/reference/dags/ingest_shipments.py @@ -0,0 +1,61 @@ +"""Hourly incremental load of carrier shipment events into the warehouse. + +Each run appends one batch file with every event that arrived since the +previous run's high-water mark (the largest ``updated_at`` it loaded, kept in +XCom) up to the end of the run's interval. Publishing the batch updates the +``shipments`` asset, which triggers ``carrier_scorecard``. +""" + +from __future__ import annotations + +import csv +from pathlib import Path + +import pendulum +from airflow.sdk import Asset, dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SOURCE = PROJECT_ROOT / "data" / "carrier_events.csv" +BATCH_DIR = PROJECT_ROOT / "output" / "warehouse" / "shipments" +SHIPMENTS = Asset("warehouse://shipments/batches") +FIELDS = ["event_id", "shipment_id", "carrier", "status", "promised_date", "updated_at"] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + max_active_runs=1, + default_args={"owner": "logistics-data", "retries": 2}, + tags=["logistics", "ingest"], +) +def ingest_shipments(): + @task(outlets=[SHIPMENTS]) + def load_batch(data_interval_end=None, ti=None) -> int: + # Airflow 3 tasks cannot query the metadata DB; read the previous + # run's high-water mark through the Task SDK XCom API instead. + # map_indexes=-1 returns the latest single value (without it, + # include_prior_dates returns a list once several runs exist). + watermark = ti.xcom_pull( + task_ids="load_batch", key="watermark", include_prior_dates=True, map_indexes=-1 + ) + upper = data_interval_end.strftime("%Y-%m-%d %H:%M:%S") + with SOURCE.open(newline="") as fh: + rows = [ + r + for r in csv.DictReader(fh) + if r["updated_at"] <= upper and (watermark is None or r["updated_at"] > watermark) + ] + out = BATCH_DIR / f"batch_{data_interval_end.strftime('%Y%m%dT%H%M')}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(rows) + ti.xcom_push(key="watermark", value=max((r["updated_at"] for r in rows), default=watermark)) + return len(rows) + + load_batch() + + +ingest_shipments() diff --git a/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/campaign_sync.py b/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/campaign_sync.py new file mode 100644 index 0000000..19bcd55 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/campaign_sync.py @@ -0,0 +1,31 @@ +"""Hourly copy of the campaign catalogue into the reporting area.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + tags=["marketing"], +) +def campaign_sync(): + @task + def copy_catalogue() -> str: + out = PROJECT_ROOT / "output" / "reference" / "campaigns.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(PROJECT_ROOT / "data" / "campaigns.csv", out) + return str(out) + + copy_catalogue() + + +campaign_sync() diff --git a/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/weekly_campaign_report.py new file mode 100644 index 0000000..c100763 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/alt_reference/dags/weekly_campaign_report.py @@ -0,0 +1,76 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, DeadlineAlert, DeadlineReference, SyncCallback +from airflow.timetables.interval import CronDataIntervalTimetable + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" +WEEK = timedelta(days=7) + + +def page_growth_oncall(context, **_): + print("SLA missed: weekly_campaign_report still running at 09:00 UTC") + + +def summarize_week(data_interval_start, data_interval_end, **_): + start, end = data_interval_start.date().isoformat(), data_interval_end.date().isoformat() + spend: dict[str, float] = defaultdict(float) + conversions: dict[str, int] = defaultdict(int) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if start <= row["event_ts"][:10] < end: + spend[row["campaign_id"]] += float(row["spend"]) + conversions[row["campaign_id"]] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(spend): + conv = conversions[cid] + cpc = f"{spend[cid] / conv:.2f}" if conv else "" + writer.writerow([cid, f"{spend[cid]:.2f}", conv, cpc]) + return {"week_start": start, "week_end": end, "campaigns": len(spend)} + + +def publish_manifest(ti, **_): + summary = ti.xcom_pull(task_ids="summarize_week") + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + +with DAG( + dag_id="weekly_campaign_report", + schedule=CronDataIntervalTimetable("0 6 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + catchup=True, + default_args={"owner": "growth-analytics", "retries": 1}, + # The logical date is the start of the reported week, so the old 3h SLA + # (measured from the Monday 06:00 run) is logical_date + 7 days + 3 hours. + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_LOGICAL_DATE, + interval=WEEK + timedelta(hours=3), + callback=SyncCallback(page_growth_oncall), + ), + max_active_runs=2, + tags=["marketing", "weekly"], +) as dag: + summarize = PythonOperator(task_id="summarize_week", python_callable=summarize_week) + publish = PythonOperator(task_id="publish_manifest", python_callable=publish_manifest) + summarize >> publish diff --git a/evals/airflow/cases/migration-weekly-catchup/case.yaml b/evals/airflow/cases/migration-weekly-catchup/case.yaml new file mode 100644 index 0000000..db0c17c --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/case.yaml @@ -0,0 +1,27 @@ +id: migration-weekly-catchup +area: migration +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + Our platform team is switching the scheduler over to Airflow 3.3 (the venv in + this repo already has 3.3.2 installed). Please port the marketing DAGs so they + run on 3.3. The growth team depends on the weekly campaign report working the + way it does now (see the README), including the season re-runs and the + on-call page when the Monday report is late. +description: | + Airflow 2.11 -> 3.3 migration of a weekly reporting project. Pain points P7 + (migration beyond ruff) and P2 (schedule semantics). `weekly_campaign_report` + has no `catchup` argument and the README's season re-run process relies on + Airflow 2's catchup=True default (Airflow 3 defaults to False and nothing + errors). Its bare cron "0 6 * * 1" plus `ds`/`next_ds` reports on the + PREVIOUS Mon-Sun week; in Airflow 3 a bare cron becomes CronTriggerTimetable + and `next_ds` is gone. `sla` in default_args and `sla_miss_callback` are + silently accepted no-ops in 3.3, so the late-report page disappears unless + it is rebuilt as a Deadline Alert. Primary: both DAGs import on 3.3, task + graph preserved, dags test succeeds, the Monday 06:00 runs replayed in 3.3 + write the same weekly CSVs/manifests as the original replayed in 2.11, the + scheduler would create the same backfill runs from start_date as 2.11, and a + Deadline Alert with a callback resolves to 09:00 UTC for the Monday run. + Secondary: verification commands, ruff AIR clean, no deprecation warnings. diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/README.md b/evals/airflow/cases/migration-weekly-catchup/fixture/README.md new file mode 100644 index 0000000..43ab5aa --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/README.md @@ -0,0 +1,17 @@ +# marketing-analytics + +| DAG | Schedule | Notes | +|---|---|---| +| `weekly_campaign_report` | Mondays 06:00 UTC | Summarises the previous Mon-Sun week per campaign into `output/weekly/.csv` and `output/manifests/.json` | +| `campaign_sync` | hourly | Copies the campaign catalogue; only the latest copy matters | + +## Re-running a season + +When growth re-opens a campaign season we deploy the report with `start_date` +set to the season's first Monday, and the scheduler produces one run for every +week since then. The dashboard expects a report for every week. + +## SLA + +The weekly report must be finished by 09:00 UTC on Monday (3h after it +starts); `notify_growth_oncall` pages the on-call analyst when it is late. diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/dags/campaign_sync.py b/evals/airflow/cases/migration-weekly-catchup/fixture/dags/campaign_sync.py new file mode 100644 index 0000000..4ae3318 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/dags/campaign_sync.py @@ -0,0 +1,31 @@ +"""Hourly copy of the campaign catalogue into the reporting area.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule_interval="@hourly", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + tags=["marketing"], +) +def campaign_sync(): + @task + def copy_catalogue() -> str: + out = PROJECT_ROOT / "output" / "reference" / "campaigns.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(PROJECT_ROOT / "data" / "campaigns.csv", out) + return str(out) + + copy_catalogue() + + +campaign_sync() diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/fixture/dags/weekly_campaign_report.py new file mode 100644 index 0000000..150cb17 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/dags/weekly_campaign_report.py @@ -0,0 +1,63 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.decorators import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(dag, task_list, blocking_task_list, slas, blocking_tis): + print(f"SLA missed for {task_list} in {dag.dag_id}") + + +@dag( + schedule_interval="0 6 * * 1", + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + default_args={"owner": "growth-analytics", "retries": 1, "sla": timedelta(hours=3)}, + sla_miss_callback=notify_growth_oncall, + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(ds=None, next_ds=None) -> dict: + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if ds <= row["event_ts"][:10] < next_ds: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": ds, "week_end": next_ds, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaign_events.csv b/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaign_events.csv new file mode 100644 index 0000000..2472f6d --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaign_events.csv @@ -0,0 +1,358 @@ +event_ts,campaign_id,spend,conversions +2026-01-01T01:27:19,C-310,798.38,4 +2026-01-01T03:00:00,C-101,159.74,11 +2026-01-01T12:14:19,C-101,446.66,1 +2026-01-01T16:13:37,C-101,853.16,9 +2026-01-01T19:34:35,C-205,386.39,9 +2026-01-02T03:00:00,C-102,553.95,9 +2026-01-02T03:54:05,C-101,466.29,9 +2026-01-02T07:38:04,C-102,765.78,8 +2026-01-02T11:22:07,C-101,726.45,8 +2026-01-02T22:57:50,C-205,660.35,1 +2026-01-03T00:23:50,C-310,125.94,6 +2026-01-03T03:00:00,C-102,639.36,11 +2026-01-03T06:07:06,C-101,712.80,6 +2026-01-03T07:56:43,C-205,411.60,3 +2026-01-03T09:20:16,C-101,236.26,1 +2026-01-03T15:59:21,C-101,18.17,7 +2026-01-04T00:23:21,C-101,851.83,7 +2026-01-04T03:00:00,C-310,173.59,10 +2026-01-04T06:08:06,C-310,542.13,6 +2026-01-04T09:43:27,C-310,593.07,5 +2026-01-04T23:33:52,C-205,896.10,8 +2026-01-05T01:56:39,C-205,385.56,9 +2026-01-05T03:00:00,C-310,207.43,10 +2026-01-05T22:13:35,C-205,876.59,7 +2026-01-06T00:27:45,C-310,217.53,2 +2026-01-06T03:00:00,C-205,115.85,4 +2026-01-06T10:14:05,C-205,404.87,6 +2026-01-06T13:02:29,C-102,740.21,9 +2026-01-06T17:48:06,C-310,755.12,2 +2026-01-06T18:48:14,C-101,614.91,1 +2026-01-07T03:00:00,C-102,852.53,1 +2026-01-07T03:32:47,C-205,616.52,10 +2026-01-07T07:19:28,C-310,716.85,10 +2026-01-07T14:04:27,C-310,64.11,0 +2026-01-07T21:55:03,C-102,466.18,2 +2026-01-08T03:00:00,C-205,896.55,0 +2026-01-08T13:08:23,C-310,624.54,4 +2026-01-08T15:35:12,C-101,443.46,0 +2026-01-08T18:05:21,C-205,688.05,0 +2026-01-09T02:31:49,C-102,875.18,9 +2026-01-09T03:00:00,C-310,415.24,3 +2026-01-09T03:24:18,C-205,662.25,0 +2026-01-09T22:22:47,C-205,277.15,7 +2026-01-10T03:00:00,C-102,57.36,4 +2026-01-10T14:04:26,C-310,832.53,4 +2026-01-10T21:10:16,C-310,517.59,4 +2026-01-10T23:06:42,C-310,292.70,2 +2026-01-11T00:26:56,C-102,643.51,6 +2026-01-11T03:00:00,C-310,99.25,7 +2026-01-11T08:52:47,C-310,469.42,0 +2026-01-12T03:00:00,C-310,708.05,11 +2026-01-12T07:18:33,C-310,607.72,0 +2026-01-12T15:01:27,C-102,846.75,4 +2026-01-12T18:02:28,C-205,526.35,5 +2026-01-12T20:23:49,C-205,438.91,2 +2026-01-13T02:20:23,C-205,497.76,2 +2026-01-13T03:00:00,C-102,565.51,7 +2026-01-13T11:50:06,C-205,806.25,4 +2026-01-13T20:52:52,C-101,656.29,0 +2026-01-14T03:00:00,C-205,500.09,3 +2026-01-14T05:00:30,C-310,23.27,10 +2026-01-14T11:51:07,C-102,658.77,2 +2026-01-14T16:27:38,C-102,322.50,1 +2026-01-14T17:14:12,C-205,650.63,8 +2026-01-14T22:55:18,C-205,837.01,9 +2026-01-15T03:00:00,C-101,410.17,10 +2026-01-15T10:03:37,C-102,127.02,6 +2026-01-15T23:22:25,C-101,31.67,7 +2026-01-16T01:12:07,C-205,627.72,2 +2026-01-16T03:00:00,C-101,839.42,10 +2026-01-16T15:16:45,C-102,408.18,7 +2026-01-16T15:17:55,C-310,243.03,4 +2026-01-16T23:00:00,C-101,533.20,9 +2026-01-16T23:16:05,C-310,195.97,11 +2026-01-17T00:09:51,C-101,566.54,7 +2026-01-17T03:00:00,C-101,782.52,2 +2026-01-17T05:16:07,C-102,518.47,7 +2026-01-17T05:25:33,C-101,454.15,10 +2026-01-18T03:00:00,C-205,364.30,11 +2026-01-18T19:46:01,C-101,853.14,9 +2026-01-18T20:13:06,C-102,446.99,1 +2026-01-19T02:50:32,C-205,408.10,3 +2026-01-19T03:00:00,C-101,166.41,0 +2026-01-19T04:36:08,C-101,558.34,11 +2026-01-19T04:44:06,C-205,532.62,8 +2026-01-19T08:17:13,C-102,39.00,2 +2026-01-19T23:53:11,C-101,258.08,4 +2026-01-20T03:00:00,C-205,327.89,0 +2026-01-20T16:03:04,C-205,308.46,11 +2026-01-20T18:53:57,C-205,896.51,9 +2026-01-21T03:00:00,C-205,565.94,9 +2026-01-21T05:21:40,C-102,782.91,5 +2026-01-21T12:26:52,C-310,811.00,10 +2026-01-21T13:38:47,C-101,212.76,2 +2026-01-21T16:14:01,C-101,335.99,11 +2026-01-21T23:11:07,C-310,481.49,7 +2026-01-22T00:28:52,C-101,258.68,5 +2026-01-22T03:00:00,C-102,572.05,5 +2026-01-22T12:41:27,C-102,193.46,1 +2026-01-23T00:53:59,C-101,617.98,3 +2026-01-23T03:00:00,C-102,212.16,5 +2026-01-23T10:37:18,C-101,263.16,7 +2026-01-23T21:38:20,C-205,507.04,11 +2026-01-23T22:27:56,C-205,252.00,7 +2026-01-24T02:39:42,C-205,778.16,3 +2026-01-24T03:00:00,C-102,262.32,2 +2026-01-24T12:38:07,C-102,509.94,9 +2026-01-25T00:48:58,C-310,315.53,11 +2026-01-25T03:00:00,C-205,22.02,10 +2026-01-25T06:09:10,C-101,831.34,8 +2026-01-25T10:25:02,C-101,498.17,0 +2026-01-25T12:08:43,C-102,570.67,2 +2026-01-26T01:02:24,C-205,544.59,7 +2026-01-26T03:00:00,C-101,451.84,6 +2026-01-26T14:08:58,C-101,172.29,11 +2026-01-26T17:59:10,C-310,719.26,3 +2026-01-26T20:57:27,C-205,878.27,1 +2026-01-26T22:43:58,C-310,228.52,5 +2026-01-27T03:00:00,C-101,69.66,2 +2026-01-27T03:03:21,C-205,327.55,6 +2026-01-27T12:48:14,C-310,500.43,2 +2026-01-27T14:28:35,C-101,786.80,3 +2026-01-27T15:39:13,C-310,772.30,1 +2026-01-27T16:07:42,C-101,276.00,7 +2026-01-28T03:00:00,C-310,871.22,10 +2026-01-28T07:23:38,C-102,787.14,11 +2026-01-28T11:38:47,C-101,278.60,9 +2026-01-28T12:45:05,C-102,713.72,5 +2026-01-28T14:22:46,C-310,130.09,3 +2026-01-28T16:37:44,C-102,690.20,4 +2026-01-29T03:00:00,C-102,887.51,6 +2026-01-29T04:21:56,C-205,183.89,4 +2026-01-29T11:15:03,C-101,527.81,5 +2026-01-29T17:28:43,C-205,871.99,11 +2026-01-29T19:28:26,C-310,515.01,11 +2026-01-30T03:00:00,C-101,804.13,2 +2026-01-30T03:47:45,C-310,166.66,3 +2026-01-30T10:38:07,C-310,162.13,2 +2026-01-30T14:01:01,C-310,465.87,4 +2026-01-30T17:52:10,C-310,93.33,4 +2026-01-30T20:15:37,C-102,641.13,11 +2026-01-31T03:00:00,C-205,889.22,11 +2026-01-31T05:23:41,C-310,538.15,10 +2026-01-31T23:12:13,C-102,281.61,4 +2026-02-01T02:30:34,C-205,705.22,5 +2026-02-01T03:00:00,C-101,746.34,1 +2026-02-01T07:45:53,C-310,721.00,2 +2026-02-01T11:48:07,C-310,434.42,7 +2026-02-01T19:37:04,C-310,421.44,10 +2026-02-02T01:40:14,C-310,688.75,10 +2026-02-02T03:00:00,C-101,346.93,9 +2026-02-02T03:22:36,C-310,129.61,2 +2026-02-02T04:20:01,C-102,696.18,6 +2026-02-02T18:24:08,C-102,234.73,2 +2026-02-03T03:00:00,C-310,353.36,11 +2026-02-03T07:29:28,C-102,826.00,8 +2026-02-03T19:06:32,C-310,718.67,3 +2026-02-03T19:32:21,C-205,110.14,3 +2026-02-04T03:00:00,C-101,700.65,3 +2026-02-04T09:03:11,C-205,55.81,1 +2026-02-04T21:51:00,C-310,400.81,1 +2026-02-05T03:00:00,C-102,742.46,11 +2026-02-05T16:21:52,C-101,473.81,3 +2026-02-05T19:23:12,C-310,102.40,6 +2026-02-06T01:28:45,C-205,379.76,5 +2026-02-06T01:48:29,C-101,440.94,6 +2026-02-06T03:00:00,C-101,511.69,9 +2026-02-06T14:25:20,C-310,892.82,6 +2026-02-06T16:42:33,C-101,183.48,3 +2026-02-06T20:46:52,C-102,841.51,1 +2026-02-07T03:00:00,C-102,357.27,7 +2026-02-07T08:44:49,C-205,842.27,2 +2026-02-07T08:56:05,C-102,452.70,3 +2026-02-07T12:47:04,C-101,582.63,9 +2026-02-07T14:32:29,C-102,131.26,10 +2026-02-07T19:59:19,C-101,294.85,2 +2026-02-08T03:00:00,C-310,31.63,3 +2026-02-08T11:18:58,C-310,202.79,6 +2026-02-08T12:29:33,C-205,627.68,6 +2026-02-08T20:16:42,C-102,719.45,0 +2026-02-09T03:00:00,C-102,424.88,9 +2026-02-09T04:26:29,C-205,811.72,1 +2026-02-09T08:33:27,C-310,706.91,1 +2026-02-09T21:04:25,C-310,441.63,6 +2026-02-09T23:32:08,C-101,79.04,6 +2026-02-10T03:00:00,C-310,277.55,7 +2026-02-10T05:06:32,C-101,814.05,1 +2026-02-10T10:53:47,C-205,87.35,7 +2026-02-11T01:07:37,C-102,559.83,5 +2026-02-11T02:44:58,C-102,820.57,1 +2026-02-11T03:00:00,C-101,531.85,1 +2026-02-11T10:51:07,C-102,486.31,4 +2026-02-11T11:45:56,C-205,677.72,3 +2026-02-12T00:18:45,C-102,109.25,0 +2026-02-12T03:00:00,C-310,816.91,2 +2026-02-12T07:15:05,C-205,210.65,9 +2026-02-12T14:36:38,C-310,792.27,5 +2026-02-12T17:33:32,C-102,654.51,3 +2026-02-12T18:49:29,C-102,249.09,4 +2026-02-13T03:00:00,C-102,453.24,8 +2026-02-13T05:00:38,C-205,857.21,6 +2026-02-13T19:34:27,C-102,286.07,2 +2026-02-14T02:49:45,C-205,361.08,3 +2026-02-14T03:00:00,C-205,669.12,4 +2026-02-14T08:06:41,C-310,811.38,8 +2026-02-14T21:01:23,C-310,568.36,1 +2026-02-15T00:54:33,C-102,132.16,3 +2026-02-15T03:00:00,C-205,364.74,8 +2026-02-15T12:17:33,C-205,771.66,0 +2026-02-16T02:47:08,C-102,828.21,10 +2026-02-16T03:00:00,C-205,553.98,2 +2026-02-16T08:35:52,C-102,704.14,2 +2026-02-16T09:54:58,C-101,174.06,8 +2026-02-16T10:33:10,C-310,591.61,1 +2026-02-16T23:22:24,C-102,142.67,2 +2026-02-17T00:45:40,C-101,614.01,11 +2026-02-17T03:00:00,C-101,715.58,3 +2026-02-17T15:28:40,C-101,548.50,4 +2026-02-18T03:00:00,C-205,382.20,5 +2026-02-18T04:35:11,C-310,804.70,6 +2026-02-18T05:34:17,C-205,393.82,3 +2026-02-18T23:00:02,C-102,651.90,1 +2026-02-19T03:00:00,C-101,528.87,6 +2026-02-19T06:49:15,C-205,807.58,0 +2026-02-19T12:02:09,C-205,779.15,8 +2026-02-19T14:51:48,C-102,658.52,7 +2026-02-20T01:46:12,C-205,77.86,8 +2026-02-20T03:00:00,C-310,463.64,9 +2026-02-20T18:12:28,C-205,709.32,5 +2026-02-20T20:49:25,C-102,660.74,4 +2026-02-21T03:00:00,C-101,466.05,8 +2026-02-21T04:02:47,C-101,74.28,3 +2026-02-21T12:20:05,C-205,808.88,4 +2026-02-22T01:31:56,C-310,444.50,2 +2026-02-22T03:00:00,C-310,714.05,3 +2026-02-22T11:14:48,C-205,383.42,2 +2026-02-22T13:33:13,C-205,106.89,6 +2026-02-22T18:55:29,C-102,66.04,5 +2026-02-22T21:45:26,C-205,542.35,5 +2026-02-23T03:00:00,C-310,71.71,8 +2026-02-23T14:06:07,C-310,559.56,5 +2026-02-23T23:23:21,C-205,215.90,7 +2026-02-24T00:41:10,C-101,889.22,6 +2026-02-24T03:00:00,C-205,214.26,6 +2026-02-24T18:32:03,C-102,638.28,3 +2026-02-25T03:00:00,C-102,440.49,10 +2026-02-25T03:16:49,C-205,56.14,9 +2026-02-25T22:15:53,C-205,594.03,2 +2026-02-26T00:53:34,C-310,332.14,4 +2026-02-26T03:00:00,C-102,607.11,11 +2026-02-26T22:43:05,C-101,206.06,7 +2026-02-26T23:36:31,C-310,761.68,8 +2026-02-27T03:00:00,C-101,487.58,1 +2026-02-27T06:26:42,C-310,110.93,7 +2026-02-27T09:44:00,C-101,761.40,7 +2026-02-27T11:37:21,C-102,40.91,3 +2026-02-27T14:30:14,C-102,27.82,9 +2026-02-27T23:39:56,C-205,113.11,6 +2026-02-28T01:38:47,C-102,199.29,2 +2026-02-28T03:00:00,C-101,681.77,8 +2026-02-28T06:54:23,C-310,498.00,3 +2026-02-28T07:43:45,C-102,736.92,1 +2026-02-28T08:57:12,C-310,899.51,10 +2026-02-28T10:13:34,C-205,413.71,4 +2026-03-01T03:00:00,C-205,297.36,1 +2026-03-01T10:41:07,C-205,174.95,5 +2026-03-01T13:41:53,C-102,635.23,5 +2026-03-01T18:00:46,C-102,181.42,9 +2026-03-02T01:19:47,C-310,859.63,5 +2026-03-02T03:00:00,C-102,237.10,3 +2026-03-02T03:54:23,C-102,74.31,11 +2026-03-02T08:48:47,C-102,85.98,4 +2026-03-02T11:37:33,C-205,40.14,3 +2026-03-02T12:25:27,C-101,539.91,2 +2026-03-03T03:00:00,C-205,223.46,6 +2026-03-03T03:15:56,C-101,841.57,2 +2026-03-03T04:30:19,C-102,603.62,6 +2026-03-03T15:41:45,C-102,188.71,0 +2026-03-03T16:11:35,C-101,567.66,10 +2026-03-04T03:00:00,C-310,121.28,3 +2026-03-04T06:47:57,C-205,278.33,4 +2026-03-04T11:57:08,C-101,554.17,7 +2026-03-04T14:51:45,C-205,719.63,5 +2026-03-04T17:06:56,C-205,661.06,6 +2026-03-05T03:00:00,C-310,848.20,5 +2026-03-05T03:19:52,C-102,143.82,0 +2026-03-05T08:11:38,C-205,493.75,1 +2026-03-05T10:59:53,C-310,637.05,1 +2026-03-05T11:24:20,C-102,154.43,1 +2026-03-05T18:23:04,C-310,860.08,7 +2026-03-06T00:20:50,C-205,419.42,6 +2026-03-06T01:18:07,C-310,288.34,0 +2026-03-06T03:00:00,C-102,320.94,9 +2026-03-06T06:16:48,C-310,418.73,7 +2026-03-07T00:24:03,C-102,728.59,7 +2026-03-07T03:00:00,C-205,760.18,5 +2026-03-07T04:23:08,C-101,856.72,6 +2026-03-07T07:45:25,C-102,448.32,3 +2026-03-08T03:00:00,C-101,693.71,2 +2026-03-08T11:44:59,C-310,816.15,4 +2026-03-08T22:51:13,C-101,325.91,2 +2026-03-09T00:42:29,C-101,93.64,5 +2026-03-09T01:53:27,C-102,343.83,8 +2026-03-09T03:00:00,C-101,523.70,0 +2026-03-09T14:22:09,C-101,470.69,8 +2026-03-09T17:44:34,C-101,695.91,7 +2026-03-09T23:58:42,C-102,406.06,2 +2026-03-10T03:00:00,C-205,156.88,1 +2026-03-10T08:18:20,C-101,545.28,5 +2026-03-10T11:10:58,C-102,20.19,0 +2026-03-10T17:33:06,C-101,412.00,2 +2026-03-10T22:31:29,C-205,192.28,8 +2026-03-11T03:00:00,C-102,474.35,7 +2026-03-11T09:16:27,C-310,879.62,9 +2026-03-11T11:47:27,C-101,806.84,10 +2026-03-12T03:00:00,C-102,634.74,7 +2026-03-12T04:12:10,C-101,84.06,3 +2026-03-12T05:10:59,C-310,869.15,4 +2026-03-12T08:06:25,C-101,303.66,1 +2026-03-12T13:22:17,C-205,194.29,2 +2026-03-13T03:00:00,C-205,331.39,8 +2026-03-13T06:47:19,C-205,142.01,9 +2026-03-13T14:11:44,C-102,742.98,10 +2026-03-13T18:52:46,C-102,315.71,8 +2026-03-14T03:00:00,C-101,699.75,9 +2026-03-14T19:28:50,C-310,538.93,6 +2026-03-14T21:37:56,C-101,812.98,2 +2026-03-15T00:06:52,C-310,672.98,2 +2026-03-15T03:00:00,C-101,337.79,7 +2026-03-15T05:52:33,C-310,843.26,2 +2026-03-15T17:32:52,C-101,152.32,11 +2026-03-16T03:00:00,C-101,511.86,9 +2026-03-16T11:01:22,C-310,713.89,9 +2026-03-16T15:45:16,C-310,879.67,0 +2026-03-17T00:33:55,C-205,124.54,5 +2026-03-17T03:00:00,C-101,601.75,1 +2026-03-17T08:03:05,C-101,640.19,10 +2026-03-17T10:50:00,C-205,145.26,0 +2026-03-17T19:21:24,C-102,195.28,5 +2026-03-17T19:27:31,C-205,203.35,0 +2026-03-18T03:00:00,C-102,321.75,3 +2026-03-18T06:50:23,C-101,178.86,2 +2026-03-18T13:35:18,C-101,890.50,1 +2026-03-19T03:00:00,C-102,683.84,8 +2026-03-19T03:10:41,C-101,634.18,2 +2026-03-19T06:22:13,C-102,629.08,11 +2026-03-19T14:17:03,C-310,201.86,9 +2026-03-19T18:58:34,C-101,423.88,7 +2026-03-20T03:00:00,C-310,228.04,9 +2026-03-20T05:34:03,C-205,312.73,7 +2026-03-20T06:23:17,C-310,426.57,4 +2026-03-20T19:26:05,C-310,521.04,0 +2026-03-21T03:00:00,C-101,378.43,8 +2026-03-21T04:17:03,C-102,232.59,5 +2026-03-21T06:22:06,C-101,19.08,5 +2026-03-21T19:39:05,C-310,129.96,6 +2026-03-21T20:56:02,C-101,300.52,0 diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaigns.csv b/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaigns.csv new file mode 100644 index 0000000..cb5859b --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/data/campaigns.csv @@ -0,0 +1,5 @@ +campaign_id,name,channel +C-101,spring_search,search +C-102,retargeting,social +C-205,brand_video,social +C-310,partner_promo,social diff --git a/evals/airflow/cases/migration-weekly-catchup/fixture/requirements.txt b/evals/airflow/cases/migration-weekly-catchup/fixture/requirements.txt new file mode 100644 index 0000000..7056ae9 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==2.11.2 +pendulum==3.2.0 diff --git a/evals/airflow/cases/migration-weekly-catchup/grade.py b/evals/airflow/cases/migration-weekly-catchup/grade.py new file mode 100644 index 0000000..65d00f5 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/grade.py @@ -0,0 +1,176 @@ +"""Grader for migration-weekly-catchup. + +The fixture is an Airflow 2.11 marketing project. ``weekly_campaign_report`` +relies on three Airflow 2 behaviours that change silently in Airflow 3: + +- no ``catchup`` argument: Airflow 2 defaulted to ``catchup=True`` and the team + re-runs whole seasons by moving ``start_date`` back (README); Airflow 3 + defaults to ``catchup=False``; +- a bare cron string with ``{{ ds }}``/``{{ next_ds }}``: the Monday 06:00 run + reports on the PREVIOUS week; under Airflow 3's CronTriggerTimetable it would + report on the week that has just started (and ``next_ds`` no longer exists); +- ``sla``/``sla_miss_callback``: accepted by Airflow 3 but no-ops, so the + "report late" page to on-call silently disappears; Deadline Alerts replace it. + +Behaviour is compared with the ORIGINAL project replayed in the 2.11 env. +""" + +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(CASE_DIR)) +import migsim # noqa: E402 + +g = migsim.g +DAG_ID = "weekly_campaign_report" +OTHER_DAG = "campaign_sync" +RUN_AFTERS = ["2026-03-09T06:00:00+00:00", "2026-03-16T06:00:00+00:00"] +EXPECTED_TASKS = {"summarize_week", "publish_manifest"} +EXPECTED_DEPS = [["summarize_week", "publish_manifest"]] +# Season backfill: every run the scheduler creates from start_date up to this point. +CATCHUP_CUTOFF = "2026-03-16T06:00:00+00:00" +# SLA: the Monday 06:00 run must be finished by 09:00 UTC. +DEADLINE_RUN_AFTER = "2026-03-09T06:00:00+00:00" +DEADLINE_EXPECTED = "2026-03-09T09:00:00+00:00" +DEADLINE_TOLERANCE_S = 300 + +CATCHUP_PROBE = f""" +import pendulum +from airflow.timetables.base import TimeRestriction +dag = get_dag({DAG_ID!r}) +tt = core_timetable(dag) +cutoff = pendulum.parse({CATCHUP_CUTOFF!r}) +restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=bool(dag.catchup)) +runs, last = [], None +for _ in range(60): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > cutoff: + break + runs.append(info.run_after.isoformat()) + last = info.data_interval +RESULT = {{"catchup": bool(dag.catchup), "timetable": type(tt).__name__, "run_afters": runs}} +""" + +DEADLINE_PROBE = f""" +import pendulum +from airflow.timetables.base import TimeRestriction +dag = get_dag({DAG_ID!r}) +tt = core_timetable(dag) +target = pendulum.parse({DEADLINE_RUN_AFTER!r}) +info, last = None, None +restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) +for _ in range(5000): + nxt = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if nxt is None or nxt.run_after > target: + break + if nxt.run_after == target: + info = nxt + break + last = nxt.data_interval +alerts = [] +for alert in (getattr(dag, "deadline", None) or []): + ref = type(alert.reference).__name__ + row = {{"reference": ref, "interval": str(alert.interval), + "callback": getattr(getattr(alert, "callback", None), "path", None)}} + base = None + if ref == "DagRunQueuedAtDeadline": + base = target # a scheduled run is queued when it becomes due + elif ref == "DagRunLogicalDateDeadline" and info is not None: + base = info.logical_date + elif ref == "FixedDatetimeDeadline": + row["due"] = None + if base is not None and hasattr(alert.interval, "total_seconds"): + row["due"] = (base + alert.interval).isoformat() + alerts.append(row) +RESULT = {{"found_run": info is not None, "alerts": alerts}} +""" + + +def snapshot_reports(ws: Path) -> dict[str, str]: + out = {} + for path, text in migsim.snapshot(ws).items(): + if not (path.startswith("weekly/") or path.startswith("manifests/")): + continue + if path.endswith(".json"): + try: + text = json.dumps(json.loads(text), sort_keys=True) + except ValueError: + text = f" {text[:200]}" + out[path] = text + return out + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + # Ground truth: the untouched Airflow 2 project under the 2.11 scheduler logic. + py2 = g.env_python("2.11") + orig_ws, orig, orig_log = migsim.run_original(CASE_DIR, DAG_ID, RUN_AFTERS) + if not migsim.runs_ok(orig): + raise SystemExit(f"case bug: original project failed in 2.11: {migsim.describe(orig)}\n{orig_log}") + expected = snapshot_reports(orig_ws) + orig_catchup, proc = g.probe_json(py2, CATCHUP_PROBE, orig_ws, env=g.airflow_env(orig_ws, env_py=py2)) + if not orig_catchup or len(orig_catchup["run_afters"]) < 5: + raise SystemExit(f"case bug: catchup probe on the original failed: {orig_catchup} {proc.tail(30)}") + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly on Airflow 3.3", + imp["ok"] and not imp["import_errors"] and {DAG_ID, OTHER_DAG} <= set(imp["dags"]), + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items()) + or f"dags: {sorted(imp['dags'])}") + grader.primary("weekly_campaign_report keeps its tasks and dependency", + dag is not None and set(dag["tasks"]) == EXPECTED_TASKS and dag["deps"] == EXPECTED_DEPS, + f"tasks={dag and dag['tasks']} deps={dag and dag['deps']}") + + if dag is None: + grader.primary("dags test succeeds (weekly_campaign_report, campaign_sync)", False, "DAG missing") + else: + shutil.rmtree(ws / "output", ignore_errors=True) + cli, cli_log = migsim.simulate(py, ws, DAG_ID, [migsim.manual("2026-03-09")]) + other, other_log = migsim.simulate(py, ws, OTHER_DAG, [migsim.manual("2026-03-09")]) + ok = migsim.runs_ok(cli) and migsim.runs_ok(other) + grader.primary("dags test succeeds (weekly_campaign_report, campaign_sync)", ok, + "" if ok else f"{DAG_ID}: {migsim.describe(cli)}\n{OTHER_DAG}: {migsim.describe(other)}\n" + f"{(cli_log if not migsim.runs_ok(cli) else other_log)[-2500:]}") + + shutil.rmtree(ws / "output", ignore_errors=True) + sim, sim_log = migsim.simulate(py, ws, DAG_ID, RUN_AFTERS, block_orm=True) + grader.primary("scheduled Monday 06:00 runs succeed on 3.3", migsim.runs_ok(sim), + migsim.describe(sim) + ("" if migsim.runs_ok(sim) else "\n" + sim_log[-2500:])) + got = snapshot_reports(ws) + grader.primary("Monday runs report on the previous week, same output as 2.11", got == expected, + f"expected {expected}\n got {got}") + + got_catchup, proc = g.probe_json(py, CATCHUP_PROBE, ws, env=g.airflow_env(ws, env_py=py)) + grader.primary( + "season backfill: scheduler creates the same weekly runs from start_date as 2.11", + bool(got_catchup) and got_catchup["run_afters"] == orig_catchup["run_afters"], + f"expected run_afters {orig_catchup['run_afters']}\n got {got_catchup or proc.tail(20)}") + + dl, proc = g.probe_json(py, DEADLINE_PROBE, ws, env=g.airflow_env(ws, env_py=py)) + import pendulum # noqa: PLC0415 (grade.py runs in the Airflow env) + + want = pendulum.parse(DEADLINE_EXPECTED) + hits = [a for a in (dl or {}).get("alerts", []) + if a.get("due") and a.get("callback") + and abs((pendulum.parse(a["due"]) - want).total_seconds()) <= DEADLINE_TOLERANCE_S] + grader.primary("late-report alert still fires at 09:00 UTC for the Monday run (Deadline Alert)", + bool(hits), f"alerts={dl and dl.get('alerts')} found_run={dl and dl.get('found_run')}" + if dl is not None else proc.tail(20)) + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/migration-weekly-catchup/migsim.py b/evals/airflow/cases/migration-weekly-catchup/migsim.py new file mode 100644 index 0000000..ffa3509 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/migsim.py @@ -0,0 +1,260 @@ +"""Migration grading helpers: replay *scheduled* DAG runs in 2.11 and 3.3. + +``airflow dags test `` gives a run the data interval of a +*manual* trigger, which differs from what the scheduler assigns (for example +``ds`` under a cron data-interval timetable). The migration graders need the +scheduler's view: "the run that fires at T processes X". ``simulate`` finds the +scheduled run whose ``run_after`` equals T via the DAG's own timetable, then +executes it in-process with ``dag.test`` while forcing that run's logical date +and data interval. + +On Airflow 3 it can also emulate the worker's metadata-DB block (Airflow 3 task +processes cannot open ORM sessions; ``dag.test`` runs in-process and does not +enforce this). Any ORM session opened while DAG-project code is on the call +stack, other than through the Task SDK's execution API, raises the same +RuntimeError a real worker raises. + +This file is identical in every ``migration-*`` case directory. +""" + +from __future__ import annotations + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(Path(__file__).resolve().parents[3] / "harness"))) +import grading as g # noqa: E402 + +_MARK = "__MIGSIM_RESULT__" + +_PROBE = r''' +import json, os, sys +P = json.loads(os.environ["MIGSIM_PARAMS"]) +import airflow, pendulum +MAJOR = int(airflow.__version__.split(".")[0]) +RESULT = {"airflow_version": airflow.__version__, "runs": [], "error": None} + +def _roots(): + ws = P["workspace"] + return tuple({ws.rstrip("/") + "/", os.path.realpath(ws).rstrip("/") + "/"}) + +def install_orm_block(): + from airflow import settings + roots = _roots() + MSG = "Direct database access via the ORM is not allowed in Airflow 3.0" + + def in_project_code(): + f = sys._getframe(2) + project = False + while f is not None: + fn = f.f_code.co_filename + if "/airflow/api_fastapi/" in fn: + return False + if fn.startswith(roots): + project = True + f = f.f_back + return project + + class Guard: + def __init__(self, real): + object.__setattr__(self, "_real", real) + def __call__(self, *a, **k): + if in_project_code(): + raise RuntimeError(MSG) + return self._real(*a, **k) + def __getattr__(self, name): + if in_project_code(): + raise RuntimeError(MSG) + return getattr(self._real, name) + + for attr in ("Session", "NonScopedSession"): + real = getattr(settings, attr, None) + if real is not None and not isinstance(real, Guard): + setattr(settings, attr, Guard(real)) + +def load_dag(dag_id): + folder = os.environ["AIRFLOW__CORE__DAGS_FOLDER"] + if MAJOR >= 3: + from airflow.dag_processing.dagbag import DagBag + bag = DagBag(dag_folder=folder) + else: + from airflow.models import DagBag + bag = DagBag(dag_folder=folder, include_examples=False) + if dag_id not in bag.dags: + raise KeyError(f"DAG {dag_id!r} not found; import errors: {bag.import_errors}") + return bag.dags[dag_id] + +def core_timetable(dag): + try: + from airflow.serialization.serialized_objects import coerce_to_core_timetable + except ImportError: + return dag.timetable + return coerce_to_core_timetable(dag.timetable) + +def find_run(dag, tt, run_after): + from airflow.timetables.base import TimeRestriction + target = pendulum.parse(run_after) + restriction = TimeRestriction(earliest=dag.start_date, latest=None, catchup=True) + last = None + for _ in range(20000): + info = tt.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + if info is None or info.run_after > target: + return None + if info.run_after == target: + return info + last = info.data_interval + return None + +def task_states(dag_id, logical_date): + from airflow.models.dagrun import DagRun + from airflow.utils.session import create_session + col = DagRun.logical_date if MAJOR >= 3 else DagRun.execution_date + with create_session() as s: + dr = s.query(DagRun).filter(DagRun.dag_id == dag_id, col == logical_date).one_or_none() + if dr is None: + return None, {} + return (str(getattr(dr.state, "value", dr.state)), + {ti.task_id if ti.map_index < 0 else f"{ti.task_id}[{ti.map_index}]": + str(getattr(ti.state, "value", ti.state)) for ti in dr.get_task_instances(session=s)}) + +def forgive_deadline_bug(row): + """Airflow 3.3.2 bug: dag.test() (and so `airflow dags test`) crashes in + DagRun.update_state for any DAG with a DeadlineAlert, after every task has + succeeded ('dict' object has no attribute 'hex' on deadline_alert). The + scheduler path is unaffected, so a run whose tasks all succeeded counts.""" + err = row.get("error") or "" + tasks = row.get("tasks") or {} + if "deadline_alert" in err and tasks and all(s == "success" for s in tasks.values()): + row.pop("error") + row["state"] = "success" + row["note"] = "forgave Airflow 3.3.2 dag.test DeadlineAlert bug" + +try: + if P.get("block_orm") and MAJOR >= 3: + install_orm_block() + dag = load_dag(P["dag_id"]) + # A failing task must fail the run now instead of sleeping through retry_delay. + for t in dag.tasks: + t.retries = 0 + tt = core_timetable(dag) + RESULT["timetable"] = type(tt).__name__ + for spec in P["runs"]: + if isinstance(spec, dict): # manual run, same semantics as `airflow dags test ` + logical = pendulum.parse(spec["manual"]) + row = {"manual": spec["manual"], "logical_date": logical.isoformat()} + RESULT["runs"].append(row) + try: + if MAJOR >= 3: + dag.test(logical_date=logical) + else: + dag.test(execution_date=logical) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + row["state"], row["tasks"] = task_states(P["dag_id"], logical) + forgive_deadline_bug(row) + continue + run_after = spec + row = {"run_after": run_after} + RESULT["runs"].append(row) + info = find_run(dag, tt, run_after) + if info is None: + row["error"] = f"no scheduled run of {P['dag_id']} fires at {run_after} ({type(tt).__name__})" + continue + row.update(logical_date=info.logical_date.isoformat(), + start=info.data_interval.start.isoformat(), end=info.data_interval.end.isoformat()) + cls = type(tt) + had = "infer_manual_data_interval" in cls.__dict__ + orig = cls.__dict__.get("infer_manual_data_interval") + cls.infer_manual_data_interval = lambda self, *a, _di=info.data_interval, **k: _di + try: + if MAJOR >= 3: + dag.test(logical_date=info.logical_date, run_after=info.run_after) + else: + dag.test(execution_date=info.logical_date) + except Exception as exc: # noqa: BLE001 + row["error"] = f"dag.test raised {type(exc).__name__}: {exc}" + finally: + if had: + cls.infer_manual_data_interval = orig + else: + del cls.infer_manual_data_interval + row["state"], row["tasks"] = task_states(P["dag_id"], info.logical_date) + forgive_deadline_bug(row) +except Exception as exc: # noqa: BLE001 + import traceback + RESULT["error"] = traceback.format_exc(limit=6) +print("__MIGSIM_RESULT__" + json.dumps(RESULT, default=str)) +''' + + +def simulate(env_py: str, workspace: Path, dag_id: str, runs: list, env: dict | None = None, + block_orm: bool = False, timeout: float = 600) -> tuple[dict, str]: + """Execute runs of ``dag_id`` in order against one metadata DB (task retries forced to 0). + + Each item of ``runs`` is either an ISO datetime T (the *scheduled* run that fires at T, + with the logical date and data interval the DAG's timetable gives it) or + ``manual(date)`` (what ``airflow dags test `` does). + Returns ``(result, log_tail)``; ``result["runs"][i]`` has ``logical_date, start, end, + state, tasks, error``. ``state == "success"`` means the run passed.""" + env = dict(env if env is not None else g.airflow_env(workspace, env_py=env_py)) + db = g.ensure_db(env_py, env) + if not db.ok: + return {"runs": [], "error": "airflow db migrate failed: " + db.tail(20)}, db.tail(40) + env["MIGSIM_PARAMS"] = json.dumps({"workspace": str(Path(workspace).resolve()), "dag_id": dag_id, + "runs": list(runs), "block_orm": block_orm}) + proc = g.run_python_in_env(env_py, _PROBE, workspace, env=env, timeout=timeout) + for line in reversed(proc.stdout.splitlines()): + if line.startswith(_MARK): + return json.loads(line[len(_MARK):]), proc.tail(80) + return {"runs": [], "error": "probe crashed: " + proc.tail(30)}, proc.tail(80) + + +def manual(logical_date: str) -> dict: + """Run spec for :func:`simulate`: a manual run like ``airflow dags test ``.""" + return {"manual": logical_date} + + +def runs_ok(result: dict) -> bool: + runs = result.get("runs") or [] + return not result.get("error") and bool(runs) and all( + r.get("state") == "success" and not r.get("error") for r in runs) + + +def describe(result: dict) -> str: + if result.get("error"): + return str(result["error"])[-1500:] + return "; ".join( + f"{r.get('run_after') or 'manual ' + str(r.get('manual'))}: " + (r.get("error") or f"state={r.get('state')} interval=[{r.get('start')}, {r.get('end')}]" + f" failed={[t for t, s in (r.get('tasks') or {}).items() if s != 'success']}") + for r in result.get("runs", [])) + + +def snapshot(root: Path, rel: str = "output") -> dict[str, str]: + """{relative path: text} for every file under ``root/rel`` (sorted, stripped).""" + base = Path(root) / rel + if not base.exists(): + return {} + return {p.relative_to(base).as_posix(): p.read_text(errors="replace").strip() + for p in sorted(base.rglob("*")) if p.is_file() and "__pycache__" not in p.parts} + + +def original_workspace(case_dir: Path) -> Path: + """Fresh copy of the pristine fixture (the Airflow 2 project).""" + dest = Path(tempfile.mkdtemp(prefix="eval-orig-")) / "ws" + shutil.copytree(Path(case_dir) / "fixture", dest, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git")) + return dest + + +def run_original(case_dir: Path, dag_id: str, runs: list, prepare=None) -> tuple[Path, dict, str]: + """Run the pristine Airflow 2 project in the 2.11 env (see :func:`simulate` for ``runs``).""" + ws = original_workspace(case_dir) + if prepare: + prepare(ws) + py2 = g.env_python("2.11") + result, log = simulate(py2, ws, dag_id, runs) + return ws, result, log diff --git a/evals/airflow/cases/migration-weekly-catchup/mutants/catchup_left_default/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/mutants/catchup_left_default/dags/weekly_campaign_report.py new file mode 100644 index 0000000..39949a2 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/mutants/catchup_left_default/dags/weekly_campaign_report.py @@ -0,0 +1,81 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import ( + CronDataIntervalTimetable, + DeadlineAlert, + DeadlineReference, + SyncCallback, + dag, + task, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(context=None, **kwargs): + print("weekly_campaign_report missed its deadline (not finished 3h after the 06:00 UTC start)") + + +@dag( + # Airflow 3 maps a bare cron string to CronTriggerTimetable (the run would + # cover the week starting at the tick). Keep the Airflow 2 semantics: the + # Monday 06:00 run covers the previous Monday-to-Sunday week. + schedule=CronDataIntervalTimetable("0 6 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + default_args={"owner": "growth-analytics", "retries": 1}, + # SLAs were removed in Airflow 3: page on-call when the run is not done + # 3 hours after it was queued (06:00 -> 09:00 UTC). + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(hours=3), + callback=SyncCallback(notify_growth_oncall), + ), + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(data_interval_start=None, data_interval_end=None) -> dict: + week_start = data_interval_start.strftime("%Y-%m-%d") + week_end = data_interval_end.strftime("%Y-%m-%d") + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if week_start <= row["event_ts"][:10] < week_end: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{week_start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": week_start, "week_end": week_end, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/migration-weekly-catchup/mutants/cron_trigger_current_week/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/mutants/cron_trigger_current_week/dags/weekly_campaign_report.py new file mode 100644 index 0000000..37992c4 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/mutants/cron_trigger_current_week/dags/weekly_campaign_report.py @@ -0,0 +1,79 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import ( + DeadlineAlert, + DeadlineReference, + SyncCallback, + dag, + task, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(context=None, **kwargs): + print("weekly_campaign_report missed its deadline (not finished 3h after the 06:00 UTC start)") + + +@dag( + schedule="0 6 * * 1", + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + # Airflow 2 defaulted to catchup=True and season re-runs rely on it. + catchup=True, + default_args={"owner": "growth-analytics", "retries": 1}, + # SLAs were removed in Airflow 3: page on-call when the run is not done + # 3 hours after it was queued (06:00 -> 09:00 UTC). + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(hours=3), + callback=SyncCallback(notify_growth_oncall), + ), + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(ds=None, logical_date=None) -> dict: + week_start = ds + week_end = (logical_date + timedelta(days=7)).strftime("%Y-%m-%d") + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if week_start <= row["event_ts"][:10] < week_end: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{week_start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": week_start, "week_end": week_end, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/migration-weekly-catchup/mutants/deadline_from_logical_date/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/mutants/deadline_from_logical_date/dags/weekly_campaign_report.py new file mode 100644 index 0000000..3cf31b5 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/mutants/deadline_from_logical_date/dags/weekly_campaign_report.py @@ -0,0 +1,83 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import ( + CronDataIntervalTimetable, + DeadlineAlert, + DeadlineReference, + SyncCallback, + dag, + task, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(context=None, **kwargs): + print("weekly_campaign_report missed its deadline (not finished 3h after the 06:00 UTC start)") + + +@dag( + # Airflow 3 maps a bare cron string to CronTriggerTimetable (the run would + # cover the week starting at the tick). Keep the Airflow 2 semantics: the + # Monday 06:00 run covers the previous Monday-to-Sunday week. + schedule=CronDataIntervalTimetable("0 6 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + # Airflow 2 defaulted to catchup=True and season re-runs rely on it. + catchup=True, + default_args={"owner": "growth-analytics", "retries": 1}, + # SLAs were removed in Airflow 3: page on-call when the run is not done + # 3 hours after it was queued (06:00 -> 09:00 UTC). + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_LOGICAL_DATE, + interval=timedelta(hours=3), + callback=SyncCallback(notify_growth_oncall), + ), + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(data_interval_start=None, data_interval_end=None) -> dict: + week_start = data_interval_start.strftime("%Y-%m-%d") + week_end = data_interval_end.strftime("%Y-%m-%d") + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if week_start <= row["event_ts"][:10] < week_end: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{week_start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": week_start, "week_end": week_end, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/migration-weekly-catchup/mutants/sla_kept_no_deadline/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/mutants/sla_kept_no_deadline/dags/weekly_campaign_report.py new file mode 100644 index 0000000..6638ce4 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/mutants/sla_kept_no_deadline/dags/weekly_campaign_report.py @@ -0,0 +1,74 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import ( + CronDataIntervalTimetable, + dag, + task, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(dag, task_list, blocking_task_list, slas, blocking_tis): + print(f"SLA missed for {task_list} in {dag.dag_id}") + + +@dag( + # Airflow 3 maps a bare cron string to CronTriggerTimetable (the run would + # cover the week starting at the tick). Keep the Airflow 2 semantics: the + # Monday 06:00 run covers the previous Monday-to-Sunday week. + schedule=CronDataIntervalTimetable("0 6 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + # Airflow 2 defaulted to catchup=True and season re-runs rely on it. + catchup=True, + default_args={"owner": "growth-analytics", "retries": 1, "sla": timedelta(hours=3)}, + sla_miss_callback=notify_growth_oncall, + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(data_interval_start=None, data_interval_end=None) -> dict: + week_start = data_interval_start.strftime("%Y-%m-%d") + week_end = data_interval_end.strftime("%Y-%m-%d") + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if week_start <= row["event_ts"][:10] < week_end: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{week_start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": week_start, "week_end": week_end, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/migration-weekly-catchup/reference/dags/campaign_sync.py b/evals/airflow/cases/migration-weekly-catchup/reference/dags/campaign_sync.py new file mode 100644 index 0000000..d65e6a7 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/reference/dags/campaign_sync.py @@ -0,0 +1,31 @@ +"""Hourly copy of the campaign catalogue into the reporting area.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +@dag( + schedule="@hourly", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + tags=["marketing"], +) +def campaign_sync(): + @task + def copy_catalogue() -> str: + out = PROJECT_ROOT / "output" / "reference" / "campaigns.csv" + out.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(PROJECT_ROOT / "data" / "campaigns.csv", out) + return str(out) + + copy_catalogue() + + +campaign_sync() diff --git a/evals/airflow/cases/migration-weekly-catchup/reference/dags/weekly_campaign_report.py b/evals/airflow/cases/migration-weekly-catchup/reference/dags/weekly_campaign_report.py new file mode 100644 index 0000000..9616061 --- /dev/null +++ b/evals/airflow/cases/migration-weekly-catchup/reference/dags/weekly_campaign_report.py @@ -0,0 +1,83 @@ +"""Weekly campaign performance report. + +Every Monday at 06:00 UTC it summarises the previous Monday-to-Sunday week +(UTC) per campaign and publishes a manifest for the growth dashboard. +""" + +from __future__ import annotations + +import csv +import json +from collections import defaultdict +from datetime import timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import ( + CronDataIntervalTimetable, + DeadlineAlert, + DeadlineReference, + SyncCallback, + dag, + task, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +EVENTS = PROJECT_ROOT / "data" / "campaign_events.csv" +OUTPUT_DIR = PROJECT_ROOT / "output" + + +def notify_growth_oncall(context=None, **kwargs): + print("weekly_campaign_report missed its deadline (not finished 3h after the 06:00 UTC start)") + + +@dag( + # Airflow 3 maps a bare cron string to CronTriggerTimetable (the run would + # cover the week starting at the tick). Keep the Airflow 2 semantics: the + # Monday 06:00 run covers the previous Monday-to-Sunday week. + schedule=CronDataIntervalTimetable("0 6 * * 1", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 5, tz="UTC"), + # Airflow 2 defaulted to catchup=True and season re-runs rely on it. + catchup=True, + default_args={"owner": "growth-analytics", "retries": 1}, + # SLAs were removed in Airflow 3: page on-call when the run is not done + # 3 hours after it was queued (06:00 -> 09:00 UTC). + deadline=DeadlineAlert( + reference=DeadlineReference.DAGRUN_QUEUED_AT, + interval=timedelta(hours=3), + callback=SyncCallback(notify_growth_oncall), + ), + max_active_runs=2, + tags=["marketing", "weekly"], +) +def weekly_campaign_report(): + @task + def summarize_week(data_interval_start=None, data_interval_end=None) -> dict: + week_start = data_interval_start.strftime("%Y-%m-%d") + week_end = data_interval_end.strftime("%Y-%m-%d") + totals: dict[str, list[float]] = defaultdict(lambda: [0.0, 0]) + with EVENTS.open(newline="") as fh: + for row in csv.DictReader(fh): + if week_start <= row["event_ts"][:10] < week_end: + totals[row["campaign_id"]][0] += float(row["spend"]) + totals[row["campaign_id"]][1] += int(row["conversions"]) + out = OUTPUT_DIR / "weekly" / f"{week_start}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["campaign_id", "spend", "conversions", "cost_per_conversion"]) + for cid in sorted(totals): + spend, conv = totals[cid] + writer.writerow([cid, f"{spend:.2f}", conv, f"{spend / conv:.2f}" if conv else ""]) + return {"week_start": week_start, "week_end": week_end, "campaigns": len(totals)} + + @task + def publish_manifest(summary: dict) -> None: + out = OUTPUT_DIR / "manifests" / f"{summary['week_start']}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(summary, sort_keys=True)) + + publish_manifest(summarize_week()) + + +weekly_campaign_report() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/alt_reference/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/alt_reference/dags/pageviews_daily.py new file mode 100644 index 0000000..7950fa4 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/alt_reference/dags/pageviews_daily.py @@ -0,0 +1,57 @@ +"""Daily page view rollup for the marketing dashboard. + +Uses a data-interval timetable anchored at 02:00 UTC: the run for UTC day D +covers [D 02:00, D+1 02:00) and starts when the interval closes, one hour after +the raw export for D lands. The date of `data_interval_start` is the day to +load. catchup backfills from the first export (2026-09-01); +max_active_tis_per_dag=1 on the only task keeps a single writer on the DuckDB +file across all runs. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +def load_pageviews(day: str) -> int: + """Replace `daily_pageviews` rows for ``day`` (YYYY-MM-DD) from its raw export.""" + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS daily_pageviews " + "(view_date DATE, page VARCHAR, views INTEGER, unique_visitors INTEGER)" + ) + con.begin() + con.execute("DELETE FROM daily_pageviews WHERE view_date = ?::DATE", [day]) + con.execute( + "INSERT INTO daily_pageviews " + "SELECT ?::DATE, page, count(*), count(DISTINCT visitor_id) " + "FROM read_csv(?, header = true) GROUP BY page", + [day, str(source)], + ) + con.commit() + return con.execute("SELECT count(*) FROM daily_pageviews WHERE view_date = ?::DATE", [day]).fetchone()[0] + + +with DAG( + dag_id="pageviews_daily", + schedule=CronDataIntervalTimetable("0 2 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 9, 1, 2, tz="UTC"), + catchup=True, + default_args={"owner": "web-analytics", "retries": 3, "retry_delay": pendulum.duration(hours=1)}, + tags=["web", "warehouse"], +): + PythonOperator( + task_id="load_day", + python_callable=load_pageviews, + op_kwargs={"day": "{{ data_interval_start | ds }}"}, + max_active_tis_per_dag=1, + ) diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/case.yaml b/evals/airflow/cases/scheduling-backfill-pageviews/case.yaml new file mode 100644 index 0000000..9c2144d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/case.yaml @@ -0,0 +1,41 @@ +id: scheduling-backfill-pageviews +area: scheduling +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + We finally got the raw page view exports back to September 1st. They're in + data/pageviews/, one file per UTC day. Can you set up `pageviews_daily` so + that when I unpause it, the scheduler fills in every day since Sept 1 on its + own and then just keeps going daily? There's nothing before Sept 1, so it + shouldn't try to load anything earlier. + + Two things worry me. The warehouse is one DuckDB file, so only one day can be + written at a time. And I have to be able to re-run any day later without the + numbers getting double counted. +description: | + Safe catchup backfill on Airflow 3.3 (pain points P2 start_date/catchup + semantics, P4 backfill-unsafe tasks, P-concurrency "one run at a time"). + The fixture has catchup=False, start_date 2026-01-01 and reads + "yesterday" from the wall clock (already delete+insert per day). With "now" + pinned to 2026-09-30 12:00 UTC, the grader checks DAG attributes (catchup; + one writer across runs = max_active_runs == 1 or a single task with + max_active_tis_per_dag == 1, since max_active_tasks is per run on 3.x; a + start_date that does not move when the clock is shifted), then executes + every run the timetable would create from start_date, as the scheduler + would, against an empty warehouse: all must succeed and load exactly + 09-01..09-29 (no earlier days, no duplicates), and no run may load a day + before its export lands (D+1 01:00 UTC, per the fixture README). It then + corrupts 09-15 and re-runs that day's run, which must repair it without + touching other days. A plain `airflow dags test` must pass. Trigger cron + + `logical_date - 1 day` with start 09-02 + max_active_runs=1 (reference) and + CronDataIntervalTimetable + `data_interval_start` + task-level + max_active_tis_per_dag=1 (alt_reference) both pass. Mutants: catchup with + the old 2026-01-01 start_date, start_date 09-01 with "previous day" logic + (first run wants 08-31), no concurrency limit, max_active_tasks=1 only + (per run on 3.x, so catchup still writes in parallel), a start_date + computed from now(), the date still taken from the wall clock, "skip days + already loaded", and loading the run's own day at 03:00 while skipping + missing exports (passes the simulation, never loads anything in + production). diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/README.md b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/README.md new file mode 100644 index 0000000..45c784c --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/README.md @@ -0,0 +1,19 @@ +# web-analytics + +Airflow 3.3 project for the marketing site analytics. Everything lands in a +single local DuckDB file, `warehouse/web.duckdb`. + +| DAG | What it does | +|---|---| +| `pageviews_daily` | Aggregates one day of raw page views into `daily_pageviews` | +| `top_pages_weekly` | Exports the week's top pages to `exports/top_pages.csv` | + +Raw exports: `data/pageviews/.csv` (UTC day, columns +`event_ts, visitor_id, page, referrer`). The web tier writes each file at about +01:00 UTC the following morning. + +`daily_pageviews(view_date, page, views, unique_visitors)` feeds the marketing +dashboard. Do not rename it or its columns. + +DuckDB allows a single writer per file: two tasks writing `web.duckdb` at the +same time fail with a lock error. diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/pageviews_daily.py new file mode 100644 index 0000000..cf27330 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/pageviews_daily.py @@ -0,0 +1,66 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. This DAG aggregates yesterday's file into +the `daily_pageviews` table (views and unique visitors per page). +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day() -> int: + day = pendulum.now("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/top_pages_weekly.py b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/top_pages_weekly.py new file mode 100644 index 0000000..5c97505 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/dags/top_pages_weekly.py @@ -0,0 +1,51 @@ +"""Weekly export of the ten most viewed pages for the content team.""" + +import csv +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" +EXPORTS_DIR = PROJECT_DIR / "exports" + + +@dag( + dag_id="top_pages_weekly", + schedule="0 6 * * 1", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "web-analytics"}, + tags=["web", "exports"], +) +def top_pages_weekly(): + @task + def export_top_pages() -> str: + con = duckdb.connect(str(WAREHOUSE), read_only=True) + try: + rows = con.execute( + """ + SELECT page, sum(views) AS views + FROM daily_pageviews + WHERE view_date >= current_date - INTERVAL 7 DAY + GROUP BY page + ORDER BY views DESC + LIMIT 10 + """ + ).fetchall() + finally: + con.close() + EXPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = EXPORTS_DIR / "top_pages.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["page", "views"]) + writer.writerows(rows) + return str(out) + + export_top_pages() + + +top_pages_weekly() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-01.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-01.csv new file mode 100644 index 0000000..d0d92f6 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-01.csv @@ -0,0 +1,33 @@ +event_ts,visitor_id,page,referrer +2026-09-01 00:27:07,v0034,/pricing, +2026-09-01 01:16:47,v0003,/blog,twitter +2026-09-01 01:27:06,v0032,/blog/airflow-3,twitter +2026-09-01 01:27:48,v0032,/docs/quickstart,twitter +2026-09-01 01:48:53,v0037,/docs/quickstart, +2026-09-01 02:21:04,v0026,/signup,twitter +2026-09-01 02:47:28,v0025,/docs/quickstart,google +2026-09-01 03:17:34,v0003,/,newsletter +2026-09-01 03:39:38,v0013,/blog,newsletter +2026-09-01 06:27:27,v0040,/docs, +2026-09-01 06:33:39,v0003,/blog/airflow-3, +2026-09-01 06:57:27,v0022,/docs/quickstart, +2026-09-01 07:49:23,v0033,/contact,newsletter +2026-09-01 08:50:58,v0001,/docs,newsletter +2026-09-01 09:47:27,v0023,/blog,newsletter +2026-09-01 09:56:15,v0026,/pricing, +2026-09-01 09:59:24,v0005,/docs, +2026-09-01 10:04:41,v0030,/signup,newsletter +2026-09-01 11:12:00,v0041,/docs, +2026-09-01 11:38:44,v0031,/docs/quickstart,twitter +2026-09-01 12:24:31,v0012,/, +2026-09-01 13:12:52,v0040,/contact, +2026-09-01 13:14:26,v0001,/signup,google +2026-09-01 14:45:47,v0041,/blog,newsletter +2026-09-01 15:09:27,v0034,/, +2026-09-01 15:51:46,v0030,/blog, +2026-09-01 16:11:30,v0033,/blog, +2026-09-01 20:04:07,v0020,/,google +2026-09-01 20:51:51,v0035,/,twitter +2026-09-01 21:16:21,v0028,/, +2026-09-01 22:31:56,v0035,/blog, +2026-09-01 23:26:33,v0027,/blog, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-02.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-02.csv new file mode 100644 index 0000000..76c87ad --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-02.csv @@ -0,0 +1,27 @@ +event_ts,visitor_id,page,referrer +2026-09-02 02:11:45,v0026,/blog/airflow-3,twitter +2026-09-02 02:25:13,v0031,/,google +2026-09-02 02:59:17,v0032,/signup,google +2026-09-02 03:25:54,v0027,/docs, +2026-09-02 05:19:48,v0043,/,google +2026-09-02 05:54:20,v0022,/, +2026-09-02 06:14:01,v0028,/blog,twitter +2026-09-02 06:17:58,v0024,/docs, +2026-09-02 07:19:57,v0034,/, +2026-09-02 08:28:33,v0001,/blog,twitter +2026-09-02 08:39:06,v0012,/,twitter +2026-09-02 12:04:39,v0016,/pricing, +2026-09-02 12:44:06,v0037,/, +2026-09-02 13:02:33,v0044,/signup,google +2026-09-02 13:38:49,v0023,/,newsletter +2026-09-02 14:50:02,v0027,/docs, +2026-09-02 15:12:11,v0036,/blog, +2026-09-02 15:40:02,v0004,/docs,google +2026-09-02 15:40:04,v0040,/docs/quickstart, +2026-09-02 18:54:16,v0042,/docs,google +2026-09-02 19:09:58,v0002,/docs, +2026-09-02 19:16:01,v0029,/signup,newsletter +2026-09-02 19:24:51,v0012,/docs, +2026-09-02 20:21:24,v0003,/docs,twitter +2026-09-02 21:10:46,v0040,/docs, +2026-09-02 21:13:00,v0040,/docs,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-03.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-03.csv new file mode 100644 index 0000000..c8348a9 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-03.csv @@ -0,0 +1,23 @@ +event_ts,visitor_id,page,referrer +2026-09-03 02:02:57,v0012,/blog,newsletter +2026-09-03 03:27:08,v0021,/blog,twitter +2026-09-03 03:44:18,v0023,/blog,newsletter +2026-09-03 04:02:37,v0027,/signup, +2026-09-03 04:05:14,v0039,/blog,twitter +2026-09-03 04:08:03,v0007,/docs,google +2026-09-03 06:34:09,v0048,/signup,newsletter +2026-09-03 07:07:23,v0017,/,twitter +2026-09-03 07:45:58,v0034,/docs, +2026-09-03 08:21:27,v0008,/docs/quickstart,google +2026-09-03 11:34:58,v0016,/pricing, +2026-09-03 12:41:44,v0021,/docs/quickstart, +2026-09-03 13:41:56,v0028,/, +2026-09-03 15:12:01,v0013,/docs,newsletter +2026-09-03 18:20:10,v0015,/pricing,google +2026-09-03 18:28:04,v0023,/signup,twitter +2026-09-03 18:49:49,v0039,/signup,google +2026-09-03 19:40:52,v0028,/,google +2026-09-03 20:12:13,v0015,/signup,google +2026-09-03 20:34:30,v0014,/docs,twitter +2026-09-03 21:03:07,v0031,/blog,twitter +2026-09-03 23:34:31,v0014,/,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-04.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-04.csv new file mode 100644 index 0000000..c60949b --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-04.csv @@ -0,0 +1,30 @@ +event_ts,visitor_id,page,referrer +2026-09-04 00:21:46,v0039,/blog, +2026-09-04 01:40:12,v0038,/docs,twitter +2026-09-04 01:49:24,v0007,/, +2026-09-04 02:18:13,v0017,/signup,google +2026-09-04 03:26:44,v0020,/docs, +2026-09-04 03:55:55,v0040,/docs,twitter +2026-09-04 04:03:41,v0039,/docs,newsletter +2026-09-04 06:06:45,v0045,/blog, +2026-09-04 06:16:59,v0038,/docs/quickstart,google +2026-09-04 07:08:00,v0017,/blog, +2026-09-04 11:05:53,v0001,/docs/quickstart,google +2026-09-04 11:49:27,v0034,/, +2026-09-04 13:21:10,v0038,/blog/airflow-3,newsletter +2026-09-04 16:09:41,v0051,/signup,newsletter +2026-09-04 16:30:05,v0004,/,twitter +2026-09-04 17:11:55,v0001,/docs,newsletter +2026-09-04 17:59:41,v0048,/, +2026-09-04 18:09:11,v0047,/,twitter +2026-09-04 18:12:59,v0049,/pricing,newsletter +2026-09-04 18:40:16,v0006,/, +2026-09-04 20:26:56,v0037,/docs, +2026-09-04 20:32:33,v0029,/, +2026-09-04 20:48:29,v0005,/,twitter +2026-09-04 21:46:47,v0027,/docs, +2026-09-04 21:53:21,v0004,/,google +2026-09-04 22:14:43,v0032,/docs/quickstart,newsletter +2026-09-04 22:36:17,v0038,/docs, +2026-09-04 22:54:58,v0007,/docs, +2026-09-04 23:31:53,v0036,/,google diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-05.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-05.csv new file mode 100644 index 0000000..d61940d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-05.csv @@ -0,0 +1,44 @@ +event_ts,visitor_id,page,referrer +2026-09-05 00:00:00,v0039,/,twitter +2026-09-05 00:15:26,v0042,/blog,twitter +2026-09-05 01:48:52,v0007,/pricing,newsletter +2026-09-05 02:13:03,v0043,/contact, +2026-09-05 02:28:34,v0044,/blog,twitter +2026-09-05 02:33:28,v0027,/blog,google +2026-09-05 02:50:03,v0015,/pricing,newsletter +2026-09-05 03:53:25,v0015,/docs,newsletter +2026-09-05 05:01:44,v0049,/docs/quickstart,google +2026-09-05 05:09:03,v0003,/pricing, +2026-09-05 05:56:05,v0020,/blog,google +2026-09-05 06:02:27,v0012,/pricing,twitter +2026-09-05 06:10:51,v0038,/pricing,twitter +2026-09-05 07:01:21,v0013,/pricing,twitter +2026-09-05 08:14:20,v0049,/pricing,newsletter +2026-09-05 09:14:16,v0009,/docs, +2026-09-05 09:25:32,v0037,/blog, +2026-09-05 10:05:56,v0048,/docs,twitter +2026-09-05 10:24:57,v0035,/,google +2026-09-05 11:06:09,v0040,/signup, +2026-09-05 11:28:58,v0027,/blog,newsletter +2026-09-05 11:31:38,v0005,/docs, +2026-09-05 11:33:56,v0019,/, +2026-09-05 13:51:31,v0017,/,google +2026-09-05 14:08:08,v0041,/signup,google +2026-09-05 14:48:47,v0005,/blog/airflow-3,twitter +2026-09-05 15:01:17,v0040,/,newsletter +2026-09-05 15:02:43,v0044,/docs,newsletter +2026-09-05 15:07:47,v0033,/blog,newsletter +2026-09-05 15:30:23,v0032,/blog,google +2026-09-05 15:30:36,v0038,/, +2026-09-05 17:50:59,v0018,/contact,google +2026-09-05 18:32:50,v0029,/docs/quickstart,twitter +2026-09-05 18:37:09,v0035,/docs,google +2026-09-05 18:56:52,v0021,/,twitter +2026-09-05 21:21:34,v0040,/,newsletter +2026-09-05 21:53:14,v0049,/pricing, +2026-09-05 21:56:46,v0018,/signup, +2026-09-05 22:26:22,v0007,/,google +2026-09-05 22:39:33,v0025,/docs,newsletter +2026-09-05 22:45:41,v0037,/docs, +2026-09-05 22:51:37,v0038,/signup, +2026-09-05 23:17:10,v0030,/signup,google diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-06.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-06.csv new file mode 100644 index 0000000..c066202 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-06.csv @@ -0,0 +1,30 @@ +event_ts,visitor_id,page,referrer +2026-09-06 01:24:15,v0043,/blog, +2026-09-06 02:03:07,v0057,/, +2026-09-06 02:29:37,v0025,/docs/quickstart, +2026-09-06 02:49:14,v0036,/docs/quickstart, +2026-09-06 04:16:15,v0014,/signup,newsletter +2026-09-06 04:41:08,v0013,/docs/quickstart, +2026-09-06 08:42:29,v0018,/pricing,google +2026-09-06 08:45:59,v0046,/docs,twitter +2026-09-06 11:16:11,v0022,/docs, +2026-09-06 12:13:07,v0007,/,twitter +2026-09-06 13:12:18,v0003,/blog,google +2026-09-06 13:43:04,v0058,/docs,newsletter +2026-09-06 15:22:53,v0008,/,twitter +2026-09-06 16:14:38,v0015,/pricing,twitter +2026-09-06 16:46:06,v0020,/, +2026-09-06 17:14:08,v0027,/, +2026-09-06 17:36:36,v0020,/pricing, +2026-09-06 17:59:16,v0058,/,twitter +2026-09-06 18:20:16,v0018,/pricing,google +2026-09-06 18:32:10,v0025,/,newsletter +2026-09-06 18:49:40,v0001,/docs,newsletter +2026-09-06 18:56:29,v0017,/blog,twitter +2026-09-06 18:58:47,v0019,/, +2026-09-06 19:53:00,v0027,/, +2026-09-06 19:57:07,v0047,/docs,newsletter +2026-09-06 21:05:55,v0005,/pricing,newsletter +2026-09-06 21:16:00,v0032,/contact,twitter +2026-09-06 23:40:45,v0045,/,google +2026-09-06 23:52:03,v0039,/,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-07.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-07.csv new file mode 100644 index 0000000..5da0e54 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-07.csv @@ -0,0 +1,43 @@ +event_ts,visitor_id,page,referrer +2026-09-07 00:07:13,v0018,/blog,newsletter +2026-09-07 00:11:29,v0012,/pricing, +2026-09-07 00:23:45,v0001,/,google +2026-09-07 01:03:15,v0032,/docs,newsletter +2026-09-07 01:20:14,v0045,/signup,twitter +2026-09-07 01:45:23,v0009,/,newsletter +2026-09-07 03:11:22,v0015,/, +2026-09-07 03:28:19,v0034,/blog/airflow-3,twitter +2026-09-07 03:32:58,v0028,/pricing, +2026-09-07 04:20:34,v0027,/blog/airflow-3,newsletter +2026-09-07 04:28:05,v0058,/blog, +2026-09-07 04:52:29,v0052,/docs/quickstart, +2026-09-07 05:00:52,v0028,/,google +2026-09-07 05:11:10,v0032,/,twitter +2026-09-07 05:45:53,v0046,/docs/quickstart,twitter +2026-09-07 06:31:42,v0044,/, +2026-09-07 06:33:22,v0034,/pricing, +2026-09-07 07:12:08,v0052,/pricing, +2026-09-07 07:50:37,v0009,/blog,twitter +2026-09-07 09:21:29,v0059,/docs, +2026-09-07 09:24:42,v0007,/contact, +2026-09-07 09:32:16,v0052,/pricing,twitter +2026-09-07 09:41:07,v0007,/docs,twitter +2026-09-07 09:45:39,v0023,/,newsletter +2026-09-07 11:48:41,v0014,/,google +2026-09-07 12:06:30,v0024,/signup,google +2026-09-07 12:28:50,v0043,/docs, +2026-09-07 14:07:12,v0031,/docs/quickstart,google +2026-09-07 15:31:12,v0051,/blog, +2026-09-07 15:36:53,v0032,/blog, +2026-09-07 16:21:26,v0033,/, +2026-09-07 17:28:08,v0007,/,newsletter +2026-09-07 17:33:31,v0060,/docs,twitter +2026-09-07 17:56:24,v0060,/docs, +2026-09-07 18:53:49,v0037,/blog,twitter +2026-09-07 19:15:02,v0057,/blog/airflow-3,newsletter +2026-09-07 19:33:34,v0043,/blog, +2026-09-07 20:21:21,v0038,/pricing,google +2026-09-07 20:55:18,v0008,/contact,google +2026-09-07 21:47:19,v0036,/blog/airflow-3, +2026-09-07 23:15:27,v0014,/blog/airflow-3,google +2026-09-07 23:58:46,v0024,/docs/quickstart,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-08.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-08.csv new file mode 100644 index 0000000..635cfad --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-08.csv @@ -0,0 +1,30 @@ +event_ts,visitor_id,page,referrer +2026-09-08 00:27:57,v0011,/blog/airflow-3,twitter +2026-09-08 02:13:36,v0062,/signup, +2026-09-08 04:10:39,v0040,/, +2026-09-08 04:37:38,v0046,/signup, +2026-09-08 04:43:50,v0032,/docs,google +2026-09-08 04:52:54,v0048,/docs/quickstart,newsletter +2026-09-08 05:36:04,v0038,/docs/quickstart, +2026-09-08 08:33:51,v0011,/blog/airflow-3, +2026-09-08 10:32:46,v0026,/docs/quickstart,twitter +2026-09-08 10:52:26,v0062,/pricing,google +2026-09-08 10:56:25,v0055,/signup,google +2026-09-08 11:10:38,v0030,/docs, +2026-09-08 11:25:04,v0049,/pricing, +2026-09-08 12:35:57,v0041,/signup,twitter +2026-09-08 13:04:31,v0016,/signup, +2026-09-08 13:38:18,v0009,/blog,newsletter +2026-09-08 14:23:27,v0026,/docs/quickstart,newsletter +2026-09-08 15:19:24,v0038,/signup,google +2026-09-08 16:02:50,v0028,/blog, +2026-09-08 16:51:25,v0032,/docs,google +2026-09-08 19:15:44,v0004,/docs, +2026-09-08 19:36:37,v0036,/signup,google +2026-09-08 20:14:30,v0019,/, +2026-09-08 20:27:40,v0062,/blog,google +2026-09-08 21:17:41,v0060,/docs,newsletter +2026-09-08 21:20:47,v0062,/, +2026-09-08 22:05:32,v0036,/blog,twitter +2026-09-08 22:22:48,v0035,/blog,newsletter +2026-09-08 22:29:22,v0052,/docs, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-09.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-09.csv new file mode 100644 index 0000000..9f144d3 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-09.csv @@ -0,0 +1,44 @@ +event_ts,visitor_id,page,referrer +2026-09-09 01:19:21,v0012,/signup, +2026-09-09 02:20:32,v0039,/blog/airflow-3,google +2026-09-09 03:57:22,v0063,/, +2026-09-09 04:05:25,v0064,/docs/quickstart, +2026-09-09 04:20:40,v0005,/signup,newsletter +2026-09-09 04:55:48,v0029,/blog/airflow-3,newsletter +2026-09-09 04:56:04,v0006,/blog, +2026-09-09 05:08:07,v0016,/docs/quickstart,google +2026-09-09 06:20:23,v0010,/, +2026-09-09 06:49:24,v0010,/pricing, +2026-09-09 07:15:30,v0066,/blog/airflow-3,twitter +2026-09-09 07:53:50,v0009,/,newsletter +2026-09-09 09:16:30,v0016,/blog, +2026-09-09 09:19:38,v0043,/docs, +2026-09-09 10:12:35,v0006,/signup,twitter +2026-09-09 11:49:22,v0007,/pricing,google +2026-09-09 12:34:49,v0019,/docs, +2026-09-09 12:38:45,v0042,/contact, +2026-09-09 12:45:20,v0012,/,newsletter +2026-09-09 13:07:14,v0015,/,twitter +2026-09-09 13:39:49,v0034,/docs,newsletter +2026-09-09 14:56:10,v0052,/,google +2026-09-09 15:16:56,v0015,/,twitter +2026-09-09 15:44:26,v0025,/signup,twitter +2026-09-09 16:06:22,v0020,/docs,newsletter +2026-09-09 16:10:41,v0047,/,twitter +2026-09-09 16:12:04,v0011,/docs/quickstart,twitter +2026-09-09 16:30:36,v0008,/blog, +2026-09-09 16:34:31,v0017,/,google +2026-09-09 17:08:52,v0008,/docs/quickstart,twitter +2026-09-09 17:16:16,v0033,/,google +2026-09-09 17:41:08,v0006,/signup, +2026-09-09 18:41:31,v0060,/blog/airflow-3, +2026-09-09 19:45:00,v0014,/docs, +2026-09-09 20:28:44,v0028,/docs,google +2026-09-09 20:39:24,v0050,/, +2026-09-09 20:45:48,v0029,/docs,newsletter +2026-09-09 23:00:05,v0027,/,newsletter +2026-09-09 23:00:19,v0032,/,newsletter +2026-09-09 23:07:43,v0013,/blog/airflow-3,twitter +2026-09-09 23:20:44,v0030,/blog, +2026-09-09 23:31:51,v0021,/,twitter +2026-09-09 23:55:07,v0020,/docs/quickstart, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-10.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-10.csv new file mode 100644 index 0000000..55174f7 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-10.csv @@ -0,0 +1,44 @@ +event_ts,visitor_id,page,referrer +2026-09-10 00:00:00,v0104,/, +2026-09-10 00:13:12,v0012,/blog, +2026-09-10 00:21:37,v0024,/pricing, +2026-09-10 00:53:55,v0058,/blog,google +2026-09-10 03:12:30,v0018,/, +2026-09-10 03:55:25,v0064,/docs/quickstart,newsletter +2026-09-10 04:44:33,v0001,/blog,twitter +2026-09-10 04:57:20,v0030,/, +2026-09-10 05:07:44,v0027,/,google +2026-09-10 06:23:25,v0036,/blog/airflow-3,google +2026-09-10 07:25:55,v0011,/blog,newsletter +2026-09-10 07:31:38,v0046,/docs/quickstart, +2026-09-10 07:45:02,v0070,/,twitter +2026-09-10 07:48:36,v0006,/,twitter +2026-09-10 08:03:19,v0021,/,twitter +2026-09-10 09:32:38,v0043,/, +2026-09-10 09:42:49,v0060,/blog/airflow-3,newsletter +2026-09-10 09:45:43,v0063,/blog, +2026-09-10 09:53:33,v0016,/signup, +2026-09-10 10:03:55,v0045,/blog/airflow-3,newsletter +2026-09-10 10:18:34,v0070,/,twitter +2026-09-10 10:49:26,v0032,/blog,google +2026-09-10 11:08:06,v0020,/,newsletter +2026-09-10 11:13:12,v0003,/docs, +2026-09-10 12:42:23,v0033,/,twitter +2026-09-10 13:19:11,v0066,/, +2026-09-10 13:53:48,v0002,/blog,twitter +2026-09-10 13:53:55,v0014,/blog/airflow-3, +2026-09-10 14:59:56,v0038,/blog,twitter +2026-09-10 17:10:17,v0005,/docs,google +2026-09-10 17:28:07,v0055,/pricing, +2026-09-10 17:37:37,v0020,/contact,google +2026-09-10 17:48:51,v0056,/pricing,twitter +2026-09-10 17:54:37,v0060,/pricing,twitter +2026-09-10 18:16:28,v0007,/pricing,google +2026-09-10 18:40:14,v0042,/signup,newsletter +2026-09-10 19:34:29,v0002,/docs, +2026-09-10 20:03:59,v0060,/docs, +2026-09-10 20:10:38,v0014,/pricing, +2026-09-10 21:52:18,v0069,/blog,twitter +2026-09-10 21:56:06,v0014,/, +2026-09-10 22:56:43,v0027,/signup,google +2026-09-10 23:50:08,v0032,/blog/airflow-3,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-11.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-11.csv new file mode 100644 index 0000000..465934b --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-11.csv @@ -0,0 +1,20 @@ +event_ts,visitor_id,page,referrer +2026-09-11 00:28:41,v0064,/docs/quickstart, +2026-09-11 02:05:54,v0018,/signup,google +2026-09-11 03:50:50,v0071,/pricing, +2026-09-11 07:14:38,v0039,/blog,twitter +2026-09-11 07:44:43,v0068,/,newsletter +2026-09-11 07:46:09,v0022,/docs, +2026-09-11 08:27:08,v0005,/docs/quickstart,newsletter +2026-09-11 08:53:47,v0072,/pricing,newsletter +2026-09-11 10:27:43,v0059,/docs, +2026-09-11 11:00:47,v0008,/,twitter +2026-09-11 12:17:53,v0013,/,twitter +2026-09-11 15:12:54,v0039,/,twitter +2026-09-11 15:34:52,v0054,/pricing, +2026-09-11 16:01:18,v0005,/docs/quickstart,google +2026-09-11 18:41:29,v0043,/pricing,google +2026-09-11 18:43:18,v0062,/, +2026-09-11 18:47:26,v0019,/docs,twitter +2026-09-11 19:46:53,v0015,/pricing, +2026-09-11 21:52:09,v0019,/docs,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-12.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-12.csv new file mode 100644 index 0000000..7c4cc05 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-12.csv @@ -0,0 +1,35 @@ +event_ts,visitor_id,page,referrer +2026-09-12 01:01:52,v0018,/,twitter +2026-09-12 01:10:16,v0010,/docs/quickstart, +2026-09-12 01:21:22,v0031,/,newsletter +2026-09-12 02:09:19,v0059,/,google +2026-09-12 02:25:47,v0067,/docs, +2026-09-12 02:41:04,v0033,/blog/airflow-3,google +2026-09-12 02:42:28,v0051,/, +2026-09-12 04:39:38,v0014,/docs, +2026-09-12 05:33:08,v0001,/,newsletter +2026-09-12 07:29:30,v0055,/docs,newsletter +2026-09-12 08:04:47,v0014,/,newsletter +2026-09-12 08:12:46,v0013,/signup,google +2026-09-12 08:20:31,v0035,/docs/quickstart,google +2026-09-12 08:22:12,v0016,/,google +2026-09-12 08:46:08,v0065,/contact,google +2026-09-12 08:56:03,v0048,/docs, +2026-09-12 10:15:32,v0056,/blog,twitter +2026-09-12 11:46:58,v0036,/blog,newsletter +2026-09-12 12:25:36,v0072,/, +2026-09-12 12:36:13,v0016,/docs/quickstart, +2026-09-12 13:13:29,v0016,/blog,google +2026-09-12 15:03:11,v0076,/,google +2026-09-12 15:42:18,v0075,/docs/quickstart, +2026-09-12 16:04:21,v0041,/blog,newsletter +2026-09-12 16:04:28,v0053,/,newsletter +2026-09-12 16:23:58,v0036,/blog,newsletter +2026-09-12 16:31:05,v0050,/,twitter +2026-09-12 18:26:51,v0072,/, +2026-09-12 20:20:12,v0020,/docs,google +2026-09-12 20:59:50,v0051,/docs/quickstart, +2026-09-12 21:54:21,v0057,/blog, +2026-09-12 22:14:17,v0010,/,google +2026-09-12 23:18:30,v0045,/,newsletter +2026-09-12 23:36:38,v0059,/blog/airflow-3,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-13.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-13.csv new file mode 100644 index 0000000..9987ef5 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-13.csv @@ -0,0 +1,33 @@ +event_ts,visitor_id,page,referrer +2026-09-13 00:16:11,v0026,/pricing,google +2026-09-13 01:22:55,v0015,/, +2026-09-13 02:33:26,v0060,/docs,twitter +2026-09-13 03:02:06,v0028,/contact, +2026-09-13 03:18:42,v0014,/, +2026-09-13 03:55:26,v0007,/docs/quickstart, +2026-09-13 06:26:57,v0030,/,newsletter +2026-09-13 06:27:50,v0009,/pricing,newsletter +2026-09-13 07:33:50,v0059,/,newsletter +2026-09-13 08:01:45,v0011,/blog/airflow-3, +2026-09-13 09:37:27,v0005,/pricing,twitter +2026-09-13 09:52:19,v0041,/docs, +2026-09-13 11:16:45,v0068,/docs,twitter +2026-09-13 11:29:51,v0009,/docs,newsletter +2026-09-13 11:45:22,v0035,/,twitter +2026-09-13 13:53:57,v0022,/pricing,google +2026-09-13 15:43:27,v0034,/docs,newsletter +2026-09-13 15:50:21,v0039,/blog/airflow-3,google +2026-09-13 15:53:38,v0066,/contact,newsletter +2026-09-13 16:11:24,v0076,/contact,newsletter +2026-09-13 16:51:39,v0062,/pricing,google +2026-09-13 17:42:48,v0026,/,newsletter +2026-09-13 17:45:01,v0010,/signup,google +2026-09-13 18:34:24,v0048,/docs/quickstart,newsletter +2026-09-13 19:08:33,v0065,/docs/quickstart,google +2026-09-13 19:11:01,v0003,/pricing,newsletter +2026-09-13 19:28:00,v0035,/pricing, +2026-09-13 19:53:57,v0003,/docs,twitter +2026-09-13 20:39:24,v0056,/, +2026-09-13 21:17:33,v0045,/pricing,newsletter +2026-09-13 21:30:29,v0052,/docs,google +2026-09-13 21:55:24,v0034,/, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-14.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-14.csv new file mode 100644 index 0000000..0aba372 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-14.csv @@ -0,0 +1,25 @@ +event_ts,visitor_id,page,referrer +2026-09-14 00:15:57,v0041,/docs,newsletter +2026-09-14 00:21:22,v0060,/docs, +2026-09-14 01:12:43,v0082,/blog, +2026-09-14 01:38:16,v0032,/docs, +2026-09-14 04:08:50,v0031,/blog/airflow-3, +2026-09-14 04:37:49,v0082,/docs, +2026-09-14 04:39:22,v0003,/pricing,google +2026-09-14 06:33:03,v0047,/contact,newsletter +2026-09-14 06:44:00,v0061,/signup,newsletter +2026-09-14 08:57:12,v0046,/contact,newsletter +2026-09-14 09:42:07,v0019,/,twitter +2026-09-14 09:48:54,v0074,/, +2026-09-14 09:58:00,v0079,/,google +2026-09-14 15:49:51,v0052,/, +2026-09-14 16:13:07,v0022,/docs,twitter +2026-09-14 17:05:02,v0023,/docs, +2026-09-14 18:03:10,v0047,/, +2026-09-14 18:19:23,v0073,/docs,newsletter +2026-09-14 20:56:19,v0070,/,google +2026-09-14 21:11:10,v0058,/signup, +2026-09-14 21:22:40,v0063,/docs,newsletter +2026-09-14 22:48:12,v0081,/, +2026-09-14 23:04:59,v0068,/docs,google +2026-09-14 23:56:47,v0025,/,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-15.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-15.csv new file mode 100644 index 0000000..713564a --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-15.csv @@ -0,0 +1,33 @@ +event_ts,visitor_id,page,referrer +2026-09-15 00:00:00,v0034,/,google +2026-09-15 01:40:46,v0072,/blog,newsletter +2026-09-15 02:23:41,v0063,/docs/quickstart, +2026-09-15 03:22:57,v0075,/docs, +2026-09-15 03:50:48,v0015,/, +2026-09-15 05:16:21,v0070,/pricing, +2026-09-15 05:42:36,v0074,/blog/airflow-3,newsletter +2026-09-15 05:54:03,v0066,/blog,newsletter +2026-09-15 06:05:24,v0063,/blog,newsletter +2026-09-15 06:31:30,v0013,/blog, +2026-09-15 06:40:19,v0042,/docs/quickstart,newsletter +2026-09-15 06:44:37,v0032,/pricing,twitter +2026-09-15 07:08:26,v0030,/docs, +2026-09-15 07:16:22,v0005,/blog,newsletter +2026-09-15 07:26:42,v0058,/blog,newsletter +2026-09-15 08:55:52,v0044,/contact,newsletter +2026-09-15 09:56:34,v0023,/pricing, +2026-09-15 10:11:01,v0062,/,newsletter +2026-09-15 10:25:22,v0052,/,twitter +2026-09-15 10:49:00,v0059,/contact,twitter +2026-09-15 11:55:44,v0036,/docs, +2026-09-15 12:25:09,v0076,/,google +2026-09-15 12:39:25,v0055,/contact,twitter +2026-09-15 13:57:50,v0052,/docs/quickstart,newsletter +2026-09-15 15:11:11,v0017,/, +2026-09-15 15:55:06,v0011,/contact,twitter +2026-09-15 17:32:37,v0037,/docs,google +2026-09-15 18:33:53,v0017,/blog,google +2026-09-15 19:41:14,v0067,/docs,newsletter +2026-09-15 21:44:23,v0006,/,newsletter +2026-09-15 22:16:59,v0021,/,newsletter +2026-09-15 23:47:49,v0032,/blog/airflow-3,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-16.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-16.csv new file mode 100644 index 0000000..c17877d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-16.csv @@ -0,0 +1,40 @@ +event_ts,visitor_id,page,referrer +2026-09-16 01:17:07,v0035,/pricing,newsletter +2026-09-16 02:06:16,v0034,/docs/quickstart, +2026-09-16 04:00:37,v0015,/docs/quickstart, +2026-09-16 04:11:57,v0078,/,newsletter +2026-09-16 04:19:40,v0058,/docs, +2026-09-16 04:45:27,v0046,/, +2026-09-16 04:56:08,v0061,/blog, +2026-09-16 05:49:37,v0037,/, +2026-09-16 06:59:46,v0074,/docs/quickstart, +2026-09-16 07:14:20,v0045,/blog, +2026-09-16 08:01:48,v0030,/,google +2026-09-16 08:08:09,v0087,/docs/quickstart,twitter +2026-09-16 08:17:59,v0063,/docs/quickstart,newsletter +2026-09-16 09:14:35,v0008,/pricing,google +2026-09-16 10:23:51,v0066,/pricing,google +2026-09-16 10:34:13,v0053,/docs, +2026-09-16 10:54:36,v0083,/, +2026-09-16 11:30:54,v0064,/docs/quickstart,twitter +2026-09-16 11:54:27,v0085,/docs,newsletter +2026-09-16 12:16:18,v0033,/,newsletter +2026-09-16 12:25:58,v0045,/docs,twitter +2026-09-16 12:27:25,v0026,/docs/quickstart,twitter +2026-09-16 12:47:10,v0050,/blog/airflow-3,google +2026-09-16 14:30:04,v0085,/docs/quickstart, +2026-09-16 14:30:15,v0008,/, +2026-09-16 15:46:00,v0052,/signup, +2026-09-16 16:05:08,v0001,/docs,newsletter +2026-09-16 16:28:20,v0074,/,twitter +2026-09-16 16:57:26,v0019,/,twitter +2026-09-16 17:21:00,v0033,/pricing,google +2026-09-16 18:10:47,v0052,/,twitter +2026-09-16 18:15:14,v0035,/docs, +2026-09-16 20:41:50,v0063,/contact, +2026-09-16 21:04:31,v0051,/pricing,newsletter +2026-09-16 21:11:31,v0070,/contact,twitter +2026-09-16 21:43:29,v0083,/signup,twitter +2026-09-16 21:53:46,v0072,/,google +2026-09-16 22:26:56,v0061,/pricing, +2026-09-16 22:55:35,v0060,/pricing, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-17.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-17.csv new file mode 100644 index 0000000..8214a54 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-17.csv @@ -0,0 +1,39 @@ +event_ts,visitor_id,page,referrer +2026-09-17 00:03:23,v0074,/blog/airflow-3,newsletter +2026-09-17 00:44:06,v0018,/blog/airflow-3,twitter +2026-09-17 01:12:39,v0031,/docs, +2026-09-17 01:43:21,v0020,/signup, +2026-09-17 01:43:54,v0055,/docs, +2026-09-17 01:48:11,v0051,/,twitter +2026-09-17 02:02:51,v0032,/blog/airflow-3, +2026-09-17 03:07:18,v0026,/docs,twitter +2026-09-17 03:31:05,v0045,/,google +2026-09-17 03:47:16,v0035,/docs/quickstart,newsletter +2026-09-17 05:27:49,v0016,/signup,twitter +2026-09-17 06:50:51,v0048,/, +2026-09-17 07:05:34,v0049,/docs,newsletter +2026-09-17 07:19:36,v0045,/signup, +2026-09-17 07:27:22,v0052,/,newsletter +2026-09-17 07:56:54,v0024,/docs/quickstart,twitter +2026-09-17 08:13:22,v0068,/docs,twitter +2026-09-17 08:32:09,v0002,/docs, +2026-09-17 09:42:22,v0064,/, +2026-09-17 10:05:35,v0038,/, +2026-09-17 10:05:40,v0030,/docs,newsletter +2026-09-17 11:05:58,v0004,/,newsletter +2026-09-17 11:18:54,v0067,/,newsletter +2026-09-17 12:19:11,v0087,/blog,twitter +2026-09-17 12:48:55,v0049,/pricing,google +2026-09-17 13:55:25,v0004,/, +2026-09-17 13:55:35,v0016,/blog, +2026-09-17 14:29:16,v0002,/,google +2026-09-17 16:24:22,v0054,/,google +2026-09-17 18:06:53,v0068,/, +2026-09-17 18:18:26,v0091,/docs, +2026-09-17 20:10:49,v0068,/, +2026-09-17 21:12:14,v0084,/pricing,google +2026-09-17 21:59:44,v0037,/docs, +2026-09-17 22:08:19,v0050,/docs/quickstart,newsletter +2026-09-17 22:38:40,v0081,/pricing,google +2026-09-17 22:57:42,v0019,/,twitter +2026-09-17 23:57:00,v0056,/blog, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-18.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-18.csv new file mode 100644 index 0000000..91c6c1d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-18.csv @@ -0,0 +1,33 @@ +event_ts,visitor_id,page,referrer +2026-09-18 00:09:41,v0056,/blog, +2026-09-18 00:13:57,v0001,/blog/airflow-3,twitter +2026-09-18 02:02:29,v0054,/pricing,newsletter +2026-09-18 02:47:17,v0014,/blog,twitter +2026-09-18 03:29:35,v0027,/docs/quickstart,google +2026-09-18 03:38:21,v0050,/, +2026-09-18 05:14:49,v0072,/docs/quickstart,google +2026-09-18 05:32:51,v0068,/, +2026-09-18 06:44:57,v0079,/blog/airflow-3,google +2026-09-18 07:07:41,v0058,/,twitter +2026-09-18 08:07:17,v0026,/,newsletter +2026-09-18 08:39:51,v0031,/docs, +2026-09-18 10:33:21,v0039,/pricing,newsletter +2026-09-18 10:34:36,v0089,/signup, +2026-09-18 11:19:55,v0091,/, +2026-09-18 12:38:29,v0083,/docs/quickstart, +2026-09-18 12:55:52,v0078,/docs, +2026-09-18 13:15:30,v0012,/docs, +2026-09-18 16:42:37,v0069,/docs,google +2026-09-18 18:31:34,v0003,/pricing,google +2026-09-18 18:54:20,v0018,/docs,google +2026-09-18 19:10:32,v0093,/, +2026-09-18 19:35:09,v0008,/signup, +2026-09-18 21:09:42,v0009,/, +2026-09-18 21:16:10,v0050,/blog/airflow-3, +2026-09-18 21:31:45,v0090,/blog/airflow-3,newsletter +2026-09-18 21:33:08,v0008,/, +2026-09-18 21:40:09,v0076,/blog, +2026-09-18 22:03:55,v0046,/blog/airflow-3,newsletter +2026-09-18 22:17:20,v0081,/signup, +2026-09-18 23:43:01,v0015,/signup,twitter +2026-09-18 23:45:38,v0020,/docs, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-19.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-19.csv new file mode 100644 index 0000000..bfb89f3 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-19.csv @@ -0,0 +1,38 @@ +event_ts,visitor_id,page,referrer +2026-09-19 01:04:48,v0037,/, +2026-09-19 02:22:14,v0078,/docs,google +2026-09-19 03:01:05,v0043,/docs,newsletter +2026-09-19 03:08:12,v0084,/blog, +2026-09-19 03:24:05,v0036,/docs/quickstart,newsletter +2026-09-19 04:15:18,v0023,/, +2026-09-19 04:27:28,v0062,/signup, +2026-09-19 05:32:29,v0061,/docs,newsletter +2026-09-19 05:50:39,v0031,/pricing, +2026-09-19 06:22:20,v0092,/,newsletter +2026-09-19 06:47:36,v0026,/contact, +2026-09-19 07:43:05,v0058,/docs/quickstart, +2026-09-19 08:03:02,v0032,/blog/airflow-3,google +2026-09-19 08:06:32,v0024,/, +2026-09-19 08:27:32,v0046,/, +2026-09-19 08:45:49,v0072,/docs/quickstart, +2026-09-19 09:19:13,v0023,/,twitter +2026-09-19 10:24:29,v0002,/blog,twitter +2026-09-19 11:23:10,v0056,/, +2026-09-19 11:33:33,v0003,/pricing, +2026-09-19 11:44:21,v0080,/docs,newsletter +2026-09-19 11:49:57,v0068,/blog/airflow-3,newsletter +2026-09-19 11:52:46,v0013,/,google +2026-09-19 11:57:40,v0037,/docs/quickstart,newsletter +2026-09-19 12:06:17,v0036,/blog/airflow-3,newsletter +2026-09-19 14:23:46,v0009,/blog,twitter +2026-09-19 15:38:30,v0035,/docs/quickstart,twitter +2026-09-19 16:13:09,v0009,/docs/quickstart,google +2026-09-19 16:19:32,v0049,/blog, +2026-09-19 18:15:48,v0035,/signup, +2026-09-19 18:23:34,v0049,/,newsletter +2026-09-19 20:41:26,v0087,/docs,twitter +2026-09-19 20:47:14,v0066,/blog/airflow-3,google +2026-09-19 20:47:26,v0038,/signup,google +2026-09-19 22:03:50,v0044,/, +2026-09-19 22:18:45,v0051,/, +2026-09-19 22:34:49,v0062,/signup,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-20.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-20.csv new file mode 100644 index 0000000..9b169e3 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-20.csv @@ -0,0 +1,47 @@ +event_ts,visitor_id,page,referrer +2026-09-20 00:00:00,v0072,/, +2026-09-20 01:46:15,v0086,/docs/quickstart,newsletter +2026-09-20 02:13:24,v0012,/docs,newsletter +2026-09-20 02:25:17,v0079,/blog,twitter +2026-09-20 02:43:39,v0044,/blog, +2026-09-20 03:00:22,v0066,/docs, +2026-09-20 03:15:49,v0063,/, +2026-09-20 03:29:42,v0022,/docs/quickstart, +2026-09-20 03:42:31,v0049,/, +2026-09-20 03:51:33,v0090,/docs,twitter +2026-09-20 05:49:43,v0020,/blog,twitter +2026-09-20 06:02:42,v0094,/signup,google +2026-09-20 06:31:59,v0074,/docs,newsletter +2026-09-20 06:50:49,v0064,/,twitter +2026-09-20 06:58:04,v0022,/contact,twitter +2026-09-20 07:12:45,v0081,/pricing,google +2026-09-20 08:21:20,v0002,/docs,twitter +2026-09-20 08:43:10,v0070,/blog, +2026-09-20 09:01:05,v0096,/docs, +2026-09-20 09:08:24,v0026,/blog,twitter +2026-09-20 09:35:32,v0036,/pricing,google +2026-09-20 10:44:33,v0061,/docs/quickstart, +2026-09-20 10:49:40,v0058,/pricing,google +2026-09-20 11:00:29,v0006,/docs,twitter +2026-09-20 11:02:11,v0010,/docs, +2026-09-20 11:44:59,v0057,/,google +2026-09-20 13:24:44,v0078,/contact, +2026-09-20 13:42:21,v0011,/, +2026-09-20 14:55:44,v0088,/, +2026-09-20 15:07:05,v0077,/, +2026-09-20 16:10:03,v0053,/,twitter +2026-09-20 16:32:07,v0017,/docs/quickstart,google +2026-09-20 17:16:47,v0052,/,google +2026-09-20 17:23:23,v0023,/, +2026-09-20 17:43:07,v0089,/docs/quickstart,newsletter +2026-09-20 17:47:46,v0059,/docs,twitter +2026-09-20 17:55:06,v0021,/signup, +2026-09-20 17:57:21,v0062,/pricing, +2026-09-20 19:32:03,v0045,/,twitter +2026-09-20 21:05:11,v0062,/,newsletter +2026-09-20 21:06:47,v0075,/,newsletter +2026-09-20 21:32:25,v0100,/signup,twitter +2026-09-20 21:32:56,v0058,/pricing, +2026-09-20 23:30:58,v0011,/pricing,google +2026-09-20 23:38:15,v0056,/docs,google +2026-09-20 23:39:37,v0056,/, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-21.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-21.csv new file mode 100644 index 0000000..686845f --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-21.csv @@ -0,0 +1,46 @@ +event_ts,visitor_id,page,referrer +2026-09-21 00:19:55,v0090,/docs, +2026-09-21 00:20:05,v0007,/, +2026-09-21 00:37:54,v0048,/,google +2026-09-21 01:08:22,v0006,/, +2026-09-21 01:19:06,v0049,/signup, +2026-09-21 01:28:57,v0005,/docs/quickstart, +2026-09-21 02:13:40,v0062,/blog/airflow-3,google +2026-09-21 02:18:50,v0099,/, +2026-09-21 02:28:08,v0040,/docs,google +2026-09-21 03:28:41,v0068,/blog/airflow-3, +2026-09-21 05:14:52,v0075,/docs, +2026-09-21 06:22:38,v0098,/,twitter +2026-09-21 06:46:12,v0001,/docs,google +2026-09-21 08:11:27,v0001,/, +2026-09-21 08:24:46,v0054,/,google +2026-09-21 09:13:12,v0092,/docs,newsletter +2026-09-21 09:27:36,v0021,/docs/quickstart,twitter +2026-09-21 09:31:48,v0018,/docs,google +2026-09-21 09:45:25,v0058,/pricing, +2026-09-21 10:37:41,v0002,/,newsletter +2026-09-21 10:37:53,v0015,/pricing,twitter +2026-09-21 11:40:54,v0071,/docs, +2026-09-21 11:42:14,v0103,/blog, +2026-09-21 12:18:36,v0025,/,twitter +2026-09-21 12:40:33,v0003,/docs, +2026-09-21 13:37:24,v0018,/, +2026-09-21 14:01:27,v0004,/signup,newsletter +2026-09-21 14:49:52,v0019,/, +2026-09-21 15:27:57,v0027,/docs, +2026-09-21 16:01:36,v0027,/docs, +2026-09-21 16:08:30,v0058,/pricing, +2026-09-21 16:48:25,v0063,/docs/quickstart,newsletter +2026-09-21 17:23:10,v0095,/docs/quickstart,twitter +2026-09-21 17:57:45,v0049,/docs, +2026-09-21 18:11:39,v0016,/blog, +2026-09-21 18:43:08,v0040,/,google +2026-09-21 19:11:58,v0099,/pricing, +2026-09-21 19:27:46,v0066,/,twitter +2026-09-21 19:47:11,v0084,/docs/quickstart,google +2026-09-21 21:23:50,v0080,/docs,newsletter +2026-09-21 21:42:09,v0050,/pricing,newsletter +2026-09-21 22:02:46,v0071,/pricing,google +2026-09-21 22:54:08,v0054,/signup,google +2026-09-21 23:27:38,v0071,/blog,twitter +2026-09-21 23:43:55,v0054,/docs,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-22.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-22.csv new file mode 100644 index 0000000..4be0ce4 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-22.csv @@ -0,0 +1,45 @@ +event_ts,visitor_id,page,referrer +2026-09-22 00:06:56,v0106,/blog, +2026-09-22 00:15:32,v0099,/pricing, +2026-09-22 00:56:49,v0069,/,google +2026-09-22 02:07:27,v0092,/docs, +2026-09-22 02:37:10,v0033,/blog,twitter +2026-09-22 02:39:37,v0033,/pricing,newsletter +2026-09-22 02:57:27,v0066,/signup, +2026-09-22 03:29:25,v0016,/blog,google +2026-09-22 04:09:09,v0067,/, +2026-09-22 06:10:35,v0081,/,twitter +2026-09-22 07:14:06,v0053,/blog/airflow-3, +2026-09-22 07:45:39,v0090,/docs, +2026-09-22 08:16:11,v0025,/blog/airflow-3,google +2026-09-22 08:57:10,v0100,/blog/airflow-3,twitter +2026-09-22 09:10:42,v0033,/blog/airflow-3, +2026-09-22 09:34:55,v0011,/,google +2026-09-22 10:05:58,v0091,/blog,google +2026-09-22 10:37:32,v0042,/docs,newsletter +2026-09-22 11:12:19,v0030,/, +2026-09-22 11:24:17,v0096,/blog,newsletter +2026-09-22 11:36:32,v0027,/docs,google +2026-09-22 12:04:51,v0081,/contact, +2026-09-22 12:06:25,v0021,/contact,twitter +2026-09-22 13:02:43,v0012,/docs, +2026-09-22 13:14:41,v0095,/pricing, +2026-09-22 14:47:49,v0070,/docs,google +2026-09-22 15:12:09,v0053,/signup,twitter +2026-09-22 15:48:54,v0041,/docs/quickstart,newsletter +2026-09-22 15:51:21,v0004,/, +2026-09-22 16:06:53,v0084,/contact, +2026-09-22 16:15:56,v0044,/docs,twitter +2026-09-22 16:32:03,v0044,/blog, +2026-09-22 16:46:23,v0090,/pricing,google +2026-09-22 16:46:26,v0039,/docs,newsletter +2026-09-22 18:30:24,v0018,/docs, +2026-09-22 20:22:29,v0092,/docs,newsletter +2026-09-22 20:44:27,v0050,/docs/quickstart, +2026-09-22 20:52:01,v0040,/docs,twitter +2026-09-22 20:53:45,v0091,/pricing,newsletter +2026-09-22 22:01:20,v0066,/contact, +2026-09-22 22:16:17,v0081,/blog,google +2026-09-22 23:00:34,v0082,/docs, +2026-09-22 23:07:13,v0058,/blog/airflow-3,twitter +2026-09-22 23:48:04,v0002,/blog/airflow-3, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-23.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-23.csv new file mode 100644 index 0000000..428d9a0 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-23.csv @@ -0,0 +1,19 @@ +event_ts,visitor_id,page,referrer +2026-09-23 00:28:17,v0032,/pricing,newsletter +2026-09-23 01:07:09,v0064,/blog/airflow-3,twitter +2026-09-23 05:16:38,v0043,/pricing,twitter +2026-09-23 07:51:33,v0108,/docs,newsletter +2026-09-23 09:20:50,v0022,/docs/quickstart,twitter +2026-09-23 10:22:22,v0015,/docs, +2026-09-23 12:58:59,v0012,/signup, +2026-09-23 15:40:35,v0058,/signup, +2026-09-23 15:49:53,v0061,/contact,twitter +2026-09-23 15:53:39,v0109,/pricing, +2026-09-23 17:30:47,v0030,/,newsletter +2026-09-23 17:49:54,v0078,/, +2026-09-23 19:31:37,v0023,/, +2026-09-23 20:21:12,v0024,/docs,newsletter +2026-09-23 21:23:27,v0090,/, +2026-09-23 22:54:32,v0031,/contact,newsletter +2026-09-23 23:37:01,v0052,/signup,google +2026-09-23 23:44:27,v0098,/blog/airflow-3,google diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-24.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-24.csv new file mode 100644 index 0000000..08f576c --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-24.csv @@ -0,0 +1,45 @@ +event_ts,visitor_id,page,referrer +2026-09-24 01:43:58,v0025,/, +2026-09-24 01:50:15,v0046,/pricing,twitter +2026-09-24 02:16:34,v0014,/docs,google +2026-09-24 02:27:02,v0047,/,twitter +2026-09-24 02:29:32,v0038,/docs, +2026-09-24 02:30:36,v0064,/docs/quickstart,twitter +2026-09-24 02:35:34,v0035,/docs/quickstart, +2026-09-24 04:31:06,v0049,/,google +2026-09-24 04:55:04,v0085,/,newsletter +2026-09-24 05:07:58,v0030,/pricing, +2026-09-24 05:27:43,v0080,/blog,newsletter +2026-09-24 05:43:21,v0054,/signup, +2026-09-24 05:49:00,v0005,/pricing,twitter +2026-09-24 07:35:35,v0044,/pricing,twitter +2026-09-24 07:51:53,v0078,/pricing,google +2026-09-24 08:19:02,v0041,/docs, +2026-09-24 09:05:20,v0078,/pricing,newsletter +2026-09-24 10:21:22,v0005,/blog/airflow-3, +2026-09-24 10:48:19,v0036,/docs/quickstart, +2026-09-24 12:01:22,v0050,/,google +2026-09-24 12:05:43,v0103,/,google +2026-09-24 12:33:32,v0098,/, +2026-09-24 12:47:30,v0006,/signup, +2026-09-24 12:53:11,v0079,/signup, +2026-09-24 13:00:35,v0042,/contact, +2026-09-24 13:39:00,v0112,/docs/quickstart,newsletter +2026-09-24 13:53:45,v0090,/signup,newsletter +2026-09-24 15:08:46,v0112,/,newsletter +2026-09-24 15:21:26,v0002,/, +2026-09-24 15:42:37,v0100,/, +2026-09-24 15:46:19,v0095,/docs,google +2026-09-24 15:55:23,v0102,/pricing,newsletter +2026-09-24 16:13:51,v0090,/signup, +2026-09-24 16:52:30,v0084,/,twitter +2026-09-24 16:59:58,v0008,/pricing,twitter +2026-09-24 17:36:35,v0063,/docs,newsletter +2026-09-24 17:57:46,v0075,/,google +2026-09-24 18:21:50,v0021,/contact, +2026-09-24 18:48:08,v0055,/,google +2026-09-24 20:27:04,v0025,/docs,newsletter +2026-09-24 21:29:02,v0102,/, +2026-09-24 21:33:38,v0081,/pricing,twitter +2026-09-24 23:05:00,v0108,/, +2026-09-24 23:18:00,v0037,/docs/quickstart,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-25.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-25.csv new file mode 100644 index 0000000..272489c --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-25.csv @@ -0,0 +1,25 @@ +event_ts,visitor_id,page,referrer +2026-09-25 00:00:00,v0053,/, +2026-09-25 00:10:09,v0066,/docs, +2026-09-25 00:46:10,v0086,/pricing,google +2026-09-25 01:44:02,v0103,/docs,google +2026-09-25 02:12:56,v0061,/pricing,google +2026-09-25 03:30:07,v0093,/pricing, +2026-09-25 04:44:27,v0115,/docs, +2026-09-25 05:20:37,v0107,/docs, +2026-09-25 06:18:56,v0026,/docs,twitter +2026-09-25 06:40:48,v0046,/blog, +2026-09-25 06:56:25,v0108,/pricing,google +2026-09-25 08:33:06,v0030,/pricing, +2026-09-25 10:05:36,v0014,/signup, +2026-09-25 10:50:43,v0103,/docs, +2026-09-25 12:53:00,v0026,/docs,newsletter +2026-09-25 13:21:42,v0003,/blog,google +2026-09-25 14:03:04,v0030,/blog,twitter +2026-09-25 17:16:17,v0072,/,newsletter +2026-09-25 18:20:21,v0085,/docs,google +2026-09-25 19:10:49,v0069,/,google +2026-09-25 21:39:05,v0057,/docs, +2026-09-25 23:33:45,v0083,/,twitter +2026-09-25 23:37:33,v0046,/,newsletter +2026-09-25 23:50:04,v0083,/, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-26.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-26.csv new file mode 100644 index 0000000..2e8e2a6 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-26.csv @@ -0,0 +1,41 @@ +event_ts,visitor_id,page,referrer +2026-09-26 00:42:23,v0075,/pricing,google +2026-09-26 01:50:22,v0004,/pricing,newsletter +2026-09-26 02:02:35,v0037,/docs,twitter +2026-09-26 02:33:03,v0099,/,google +2026-09-26 02:49:03,v0011,/docs,twitter +2026-09-26 03:47:33,v0003,/,newsletter +2026-09-26 04:01:12,v0077,/signup, +2026-09-26 04:23:17,v0014,/signup, +2026-09-26 04:28:39,v0102,/,google +2026-09-26 04:49:20,v0050,/docs/quickstart, +2026-09-26 05:25:00,v0083,/docs/quickstart,google +2026-09-26 06:00:56,v0112,/,google +2026-09-26 07:36:03,v0055,/contact, +2026-09-26 08:32:17,v0055,/contact, +2026-09-26 08:55:20,v0082,/, +2026-09-26 10:38:12,v0021,/docs/quickstart, +2026-09-26 10:41:17,v0035,/,twitter +2026-09-26 11:08:46,v0006,/docs,twitter +2026-09-26 11:17:30,v0051,/, +2026-09-26 12:22:44,v0047,/signup,newsletter +2026-09-26 12:28:15,v0046,/,google +2026-09-26 13:02:04,v0086,/docs/quickstart, +2026-09-26 13:12:56,v0060,/,twitter +2026-09-26 14:17:09,v0100,/,newsletter +2026-09-26 14:17:54,v0044,/, +2026-09-26 14:49:22,v0039,/blog/airflow-3,newsletter +2026-09-26 15:27:09,v0053,/,twitter +2026-09-26 16:50:49,v0109,/docs,newsletter +2026-09-26 17:59:58,v0068,/,newsletter +2026-09-26 18:14:29,v0066,/contact,twitter +2026-09-26 18:45:41,v0012,/pricing, +2026-09-26 20:16:04,v0100,/docs, +2026-09-26 21:19:00,v0118,/signup, +2026-09-26 22:28:03,v0026,/docs,twitter +2026-09-26 22:41:21,v0031,/signup,twitter +2026-09-26 22:44:55,v0041,/blog/airflow-3, +2026-09-26 22:52:09,v0057,/, +2026-09-26 23:34:15,v0022,/,twitter +2026-09-26 23:45:28,v0044,/docs/quickstart,twitter +2026-09-26 23:50:51,v0018,/,twitter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-27.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-27.csv new file mode 100644 index 0000000..9ccad16 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-27.csv @@ -0,0 +1,21 @@ +event_ts,visitor_id,page,referrer +2026-09-27 02:18:21,v0014,/, +2026-09-27 02:33:16,v0080,/signup, +2026-09-27 02:47:05,v0112,/docs,newsletter +2026-09-27 03:39:40,v0033,/docs/quickstart, +2026-09-27 04:09:22,v0036,/docs/quickstart, +2026-09-27 06:07:15,v0120,/blog, +2026-09-27 06:50:05,v0098,/, +2026-09-27 09:31:05,v0032,/, +2026-09-27 12:33:46,v0111,/docs, +2026-09-27 12:49:26,v0011,/pricing, +2026-09-27 13:10:46,v0038,/pricing, +2026-09-27 13:55:22,v0121,/,twitter +2026-09-27 15:26:08,v0045,/,newsletter +2026-09-27 16:43:48,v0074,/signup,newsletter +2026-09-27 18:42:35,v0120,/,newsletter +2026-09-27 19:17:32,v0031,/pricing,google +2026-09-27 21:32:23,v0059,/docs,google +2026-09-27 21:47:53,v0007,/, +2026-09-27 22:54:09,v0040,/docs,newsletter +2026-09-27 23:37:55,v0011,/docs,google diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-28.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-28.csv new file mode 100644 index 0000000..2110678 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-28.csv @@ -0,0 +1,30 @@ +event_ts,visitor_id,page,referrer +2026-09-28 00:33:58,v0069,/, +2026-09-28 00:45:57,v0033,/pricing,twitter +2026-09-28 00:56:36,v0029,/docs,google +2026-09-28 01:12:36,v0073,/,newsletter +2026-09-28 03:21:32,v0114,/signup,twitter +2026-09-28 03:25:00,v0018,/pricing,google +2026-09-28 03:28:24,v0006,/, +2026-09-28 06:02:24,v0064,/blog,newsletter +2026-09-28 06:54:39,v0120,/,google +2026-09-28 08:03:23,v0020,/,newsletter +2026-09-28 08:05:16,v0023,/docs/quickstart,google +2026-09-28 08:33:46,v0103,/, +2026-09-28 11:12:16,v0102,/signup,google +2026-09-28 11:20:23,v0116,/blog, +2026-09-28 12:11:41,v0098,/docs,newsletter +2026-09-28 14:38:56,v0093,/blog,newsletter +2026-09-28 14:53:00,v0083,/,newsletter +2026-09-28 16:42:20,v0018,/docs, +2026-09-28 17:27:51,v0036,/,twitter +2026-09-28 18:25:10,v0114,/docs,google +2026-09-28 18:58:53,v0041,/docs/quickstart, +2026-09-28 19:34:26,v0096,/, +2026-09-28 20:38:58,v0034,/docs/quickstart,twitter +2026-09-28 20:39:33,v0120,/,newsletter +2026-09-28 21:56:26,v0036,/,newsletter +2026-09-28 22:12:01,v0020,/blog,twitter +2026-09-28 23:08:46,v0078,/blog/airflow-3, +2026-09-28 23:22:19,v0110,/docs, +2026-09-28 23:28:47,v0082,/docs,newsletter diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-29.csv b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-29.csv new file mode 100644 index 0000000..0d62d30 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/data/pageviews/2026-09-29.csv @@ -0,0 +1,30 @@ +event_ts,visitor_id,page,referrer +2026-09-29 00:59:15,v0085,/docs/quickstart, +2026-09-29 02:41:31,v0089,/docs, +2026-09-29 03:24:38,v0065,/pricing, +2026-09-29 03:32:19,v0016,/contact,twitter +2026-09-29 04:52:27,v0005,/blog,newsletter +2026-09-29 05:00:27,v0090,/docs, +2026-09-29 05:25:38,v0060,/, +2026-09-29 06:07:52,v0086,/,twitter +2026-09-29 06:08:27,v0080,/docs, +2026-09-29 06:25:27,v0097,/, +2026-09-29 06:38:17,v0083,/signup,google +2026-09-29 07:16:29,v0089,/, +2026-09-29 07:36:34,v0046,/pricing, +2026-09-29 08:42:53,v0102,/docs, +2026-09-29 09:00:08,v0044,/blog/airflow-3,twitter +2026-09-29 09:22:31,v0093,/pricing,twitter +2026-09-29 10:02:19,v0064,/docs/quickstart, +2026-09-29 10:40:57,v0057,/blog/airflow-3,newsletter +2026-09-29 11:09:14,v0014,/pricing, +2026-09-29 11:16:42,v0083,/,twitter +2026-09-29 11:18:42,v0009,/docs/quickstart,google +2026-09-29 12:03:59,v0115,/blog,google +2026-09-29 13:07:00,v0076,/blog, +2026-09-29 14:30:28,v0118,/blog,twitter +2026-09-29 15:01:36,v0107,/blog, +2026-09-29 16:08:21,v0107,/blog/airflow-3, +2026-09-29 17:53:16,v0072,/docs,newsletter +2026-09-29 22:09:04,v0077,/, +2026-09-29 22:59:46,v0071,/docs/quickstart, diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/fixture/requirements.txt b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/requirements.txt new file mode 100644 index 0000000..e5feecf --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +duckdb==1.5.6 diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/grade.py b/evals/airflow/cases/scheduling-backfill-pageviews/grade.py new file mode 100644 index 0000000..21fc89e --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/grade.py @@ -0,0 +1,376 @@ +"""Grader for scheduling-backfill-pageviews (Airflow 3.3). + +The user wants the scheduler to backfill every day since 2026-09-01 on its own +when the DAG is unpaused, one writer at a time, with re-runs that never double +count. "Now" is pinned to 2026-09-30 12:00 UTC (exports exist for 09-01..09-29). + +Primary checks: +- DAG attributes: catchup enabled; at most one writer at a time across runs + (max_active_runs == 1, or a single-task DAG with max_active_tis_per_dag == 1; + max_active_tasks alone is per run on 3.x and does not serialise catchup); + start_date is a fixed value (parsing under a clock shifted by 400 days + yields the same start_date). +- Backfill: every run the timetable creates from start_date up to "now" is + executed in order, as the scheduler would create it (own logical date and + data interval, via ``dag.test``), against an empty warehouse. All runs must + succeed and the warehouse must hold exactly the correct rows for 09-01..09-29 + and nothing else. +- Timing: each run only loads days whose export had landed (D+1 01:00 UTC per + the fixture README) at the run's run_after. The simulation has every file + already, so this catches schedules that would skip or fail in production. +- Re-run: the day 09-15 is corrupted (duplicated rows, stale counts) and the + run that loaded it is executed again; it must repair 09-15 and leave every + other day untouched. +- A plain ``airflow dags test`` succeeds. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +from collections import defaultdict +from datetime import date, datetime, time, timedelta, timezone +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "pageviews_daily" +NOW = "2026-09-30T12:00:00+00:00" +FIRST_DAY, LAST_DAY = date(2026, 9, 1), date(2026, 9, 29) +REPAIR_DAY = "2026-09-15" +MAX_RUNS = 40 # 29 days are expected; anything far above means start_date is too early +WAREHOUSE_REL = Path("warehouse") / "web.duckdb" +EXPORT_LANDS_HOUR_UTC = 1 # fixture README: each day's export is written at about 01:00 UTC the next morning +EXPECTED_DAYS = [(FIRST_DAY + timedelta(days=i)).isoformat() for i in range((LAST_DAY - FIRST_DAY).days + 1)] + + +def expected_rows() -> dict[str, dict[str, tuple[int, int]]]: + """{day: {page: (views, unique_visitors)}} from the pristine fixture exports.""" + out: dict[str, dict[str, tuple[int, int]]] = {} + for f in sorted((CASE_DIR / "fixture" / "data" / "pageviews").glob("*.csv")): + views: dict[str, int] = defaultdict(int) + visitors: dict[str, set] = defaultdict(set) + with f.open(newline="") as fh: + for row in csv.DictReader(fh): + views[row["page"]] += 1 + visitors[row["page"]].add(row["visitor_id"]) + out[f.stem] = {p: (views[p], len(visitors[p])) for p in views} + return out + + +def check_warehouse(rows: list | None, exp: dict) -> tuple[bool, str]: + if rows is None: + return False, "daily_pageviews unreadable" + by_day: dict[str, list] = defaultdict(list) + for d, page, views, uniq in rows: + by_day[d].append((page, views, uniq)) + problems = [] + extra = sorted(set(by_day) - set(EXPECTED_DAYS)) + if extra: + problems.append(f"rows for days outside 09-01..09-29: {extra[:5]}{'...' if len(extra) > 5 else ''}") + missing = [d for d in EXPECTED_DAYS if d not in by_day] + if missing: + problems.append(f"missing days {missing[:8]}{'...' if len(missing) > 8 else ''} ({len(missing)} total)") + wrong = [] + for d in EXPECTED_DAYS: + if d not in by_day: + continue + pages = [p for p, _, _ in by_day[d]] + got = {p: (v, u) for p, v, u in by_day[d]} + if len(pages) != len(set(pages)) or got != exp[d]: + wrong.append(d) + if wrong: + problems.append(f"wrong or duplicated rows for {wrong[:8]}") + return not problems, "; ".join(problems) + + +# --------------------------------------------------------------------------- +# Probes +# --------------------------------------------------------------------------- + +_ATTR_CODE = r''' +dag = get_dag(DAG_ID) +RESULT = {"catchup": dag.catchup, "max_active_runs": dag.max_active_runs, + "max_active_tasks": getattr(dag, "max_active_tasks", None), + "task_limits": {t.task_id: getattr(t, "max_active_tis_per_dag", None) for t in dag.tasks}, + "start_date": str(dag.start_date), "end_date": str(dag.end_date)} +''' + + +def single_writer(attrs: dict) -> tuple[bool, str]: + """At most one task instance of the DAG can write the DuckDB file at a time, across runs. + + ``max_active_runs == 1`` serialises runs. A single-task DAG whose task has + ``max_active_tis_per_dag == 1`` is serialised across runs too. On Airflow 3.x + ``max_active_tasks`` is enforced per DAG run, so on its own it still lets + catchup run up to ``max_active_runs`` writers in parallel. A pool is not + accepted: it only exists once someone creates it in the metadata DB. + """ + limits = attrs.get("task_limits") or {} + ok = attrs["max_active_runs"] == 1 or (len(limits) == 1 and list(limits.values()) == [1]) + return ok, (f"max_active_runs={attrs['max_active_runs']}, max_active_tasks={attrs['max_active_tasks']} " + f"(per run on 3.x), task max_active_tis_per_dag={limits}") + +# Parse the DAG under a clock shifted forward by 400 days. +_SHIFTED_CLOCK_CODE = r''' +import datetime as _dt +import pendulum +SHIFT = _dt.timedelta(days=400) +_Real, _RealDate = _dt.datetime, _dt.date + +# Like freezegun: isinstance() against the replacement classes still accepts real instances. +class _DTMeta(type): + def __instancecheck__(cls, obj): + return isinstance(obj, _Real) + def __subclasscheck__(cls, sub): + return issubclass(sub, _Real) + +class _DateMeta(type): + def __instancecheck__(cls, obj): + return isinstance(obj, _RealDate) + def __subclasscheck__(cls, sub): + return issubclass(sub, _RealDate) + +class _ShiftedDateTime(_Real, metaclass=_DTMeta): + @classmethod + def now(cls, tz=None): + return _Real.now(tz) + SHIFT + @classmethod + def utcnow(cls): + return _Real.utcnow() + SHIFT + @classmethod + def today(cls): + return _Real.today() + SHIFT + +class _ShiftedDate(_RealDate, metaclass=_DateMeta): + @classmethod + def today(cls): + return _RealDate.fromordinal(_Real.today().toordinal() + 400) + +_dt.datetime = _ShiftedDateTime +_dt.date = _ShiftedDate +_pnow = pendulum.now +pendulum.now = lambda tz="local": _pnow(tz).add(days=400) +pendulum.today = lambda tz="local": _pnow(tz).add(days=400).start_of("day") +pendulum.yesterday = lambda tz="local": _pnow(tz).add(days=399).start_of("day") +for modname in ("airflow.utils.timezone", "airflow.sdk.timezone"): + try: + mod = __import__(modname, fromlist=["utcnow"]) + _u = mod.utcnow + mod.utcnow = lambda _u=_u: _u() + SHIFT + except Exception: + pass +dag = get_dag(DAG_ID) +RESULT = {"start_date": str(dag.start_date)} +''' + +# Execute scheduled runs (the timetable's own logical date and data interval). +# MODE "backfill": every run from start_date with run_after <= NOW, recording the +# days each run added. MODE "single": only run number RUN_INDEX of that sequence. +_RUNS_CODE = r''' +import duckdb, pathlib, pendulum +from airflow.timetables.base import DataInterval, TimeRestriction +dag = get_dag(DAG_ID) +tt = core_timetable(dag) +for t in dag.tasks: + t.retries = 0 # a failed run must fail fast instead of waiting for retry_delay +wh = pathlib.Path(WAREHOUSE) +latest = pendulum.parse(NOW) +infos, last = [], None +for _ in range(MAX_RUNS + 1): + info = tt.next_dagrun_info(last_automated_data_interval=last, + restriction=TimeRestriction(earliest=dag.start_date, latest=dag.end_date, catchup=True)) + if info is None or info.run_after > latest: + break + infos.append(info) + last = info.data_interval + +def days(): + if not wh.exists(): + return set() + con = duckdb.connect(str(wh), read_only=True) + try: + return {str(r[0]) for r in con.execute("SELECT DISTINCT CAST(view_date AS VARCHAR) FROM daily_pageviews").fetchall()} + except Exception: + return set() + finally: + con.close() + +out = {"n_runs": len(infos), + "first": [str(infos[0].logical_date), str(infos[0].data_interval.start), str(infos[0].run_after)] if infos else None, + "runs": []} +if len(infos) <= MAX_RUNS: + todo = list(enumerate(infos)) if MODE == "backfill" else [(RUN_INDEX, infos[RUN_INDEX])] + failures = 0 + for i, info in todo: + di = info.data_interval + patched = lambda self, run_after, di=di: DataInterval(di.start, di.end) + type(tt).infer_manual_data_interval = patched + type(dag.timetable).infer_manual_data_interval = patched + before = days() + rec = {"i": i, "logical_date": str(info.logical_date), "interval": [str(di.start), str(di.end)], + "run_after": pendulum.instance(info.run_after).in_timezone("UTC").isoformat()} + try: + dr = dag.test(logical_date=info.logical_date) + rec["state"] = str(getattr(dr.state, "value", dr.state)) if dr is not None else None + except Exception as exc: + rec["state"] = "error: " + repr(exc)[:300] + rec["new_days"] = sorted(days() - before) + out["runs"].append(rec) + if rec["state"] != "success": + failures += 1 + if failures >= 3: + out["aborted"] = "stopped after 3 failed runs" + break +RESULT = out +''' + +_READ_CODE = r''' +import duckdb, json, sys +con = duckdb.connect(sys.argv[1], read_only=True) +rows = con.execute("SELECT CAST(view_date AS VARCHAR), page, views, unique_visitors FROM daily_pageviews").fetchall() +print("__WH__" + json.dumps([[str(a), str(b), int(c), int(d)] for a, b, c, d in rows])) +''' + +_CORRUPT_CODE = r''' +import duckdb, sys +con = duckdb.connect(sys.argv[1]) +day = sys.argv[2] +# A crashed or doubled attempt: the day's rows are duplicated and one count is stale. +con.execute("INSERT INTO daily_pageviews SELECT * FROM daily_pageviews WHERE CAST(view_date AS VARCHAR) = ?", [day]) +con.execute("UPDATE daily_pageviews SET views = views + 7 WHERE CAST(view_date AS VARCHAR) = ? AND page = '/'", [day]) +con.close() +print("CORRUPTED") +''' + + +def loads_after_export_lands(runs: list[dict]) -> tuple[bool, str]: + """Every run loads only days whose export had landed (next day 01:00 UTC) when the run starts. + + Under catchup every export already exists, so a run scheduled before its + day's export lands still succeeds in the simulation; in production it would + fail, or skip and never load the day.""" + loaded = [(r["run_after"], d) for r in runs if r.get("state") == "success" for d in r.get("new_days", [])] + if not loaded: + return False, "no run loaded any day" + early = [] + for run_after, d in loaded: + lands = datetime.combine(date.fromisoformat(d) + timedelta(days=1), time(EXPORT_LANDS_HOUR_UTC), + tzinfo=timezone.utc) + if datetime.fromisoformat(run_after) < lands: + early.append(f"{d} loaded by the run at {run_after}") + return not early, (f"export for day D lands at D+1 {EXPORT_LANDS_HOUR_UTC:02d}:00 UTC; " + + ("; ".join(early[:5]) if early else f"{len(loaded)} day loads checked")) + + +def migrated_env(py: str, ws: Path) -> dict: + env = g.airflow_env(ws) + db = g.ensure_db(py, env) + if not db.ok: + raise RuntimeError("airflow db migrate failed: " + db.tail(20)) + return env + + +def execute_runs(py: str, ws: Path, mode: str, run_index: int = 0) -> tuple[dict | None, str]: + code = (f"DAG_ID = {DAG_ID!r}\nNOW = {NOW!r}\nMAX_RUNS = {MAX_RUNS}\nMODE = {mode!r}\n" + f"RUN_INDEX = {run_index}\nWAREHOUSE = {str(ws / WAREHOUSE_REL)!r}\n" + _RUNS_CODE) + res, proc = g.probe_json(py, code, ws, env=migrated_env(py, ws), timeout=900) + return res, proc.tail(40) + + +def read_rows(py: str, ws: Path) -> list | None: + db = ws / WAREHOUSE_REL + if not db.exists(): + return None + res = g.run_cmd([py, "-c", _READ_CODE, str(db)], env=g.scrubbed_environ(), timeout=120) + for line in res.stdout.splitlines(): + if line.startswith("__WH__"): + return json.loads(line[6:]) + return None + + +def main() -> None: + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + exp = expected_rows() + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + + if dag is not None: + attrs, proc = g.probe_json(py, f"DAG_ID = {DAG_ID!r}\n" + _ATTR_CODE, ws) + if attrs is None: + grader.primary("catchup enabled", False, "attribute probe failed: " + proc.tail(20)) + grader.primary("one writer at a time", False, "attribute probe failed") + else: + grader.primary("catchup enabled", attrs["catchup"] is True, f"catchup={attrs['catchup']!r}") + grader.primary("one writer at a time", *single_writer(attrs)) + shifted, proc = g.probe_json(py, f"DAG_ID = {DAG_ID!r}\n" + _SHIFTED_CLOCK_CODE, ws) + if shifted is None: + grader.primary("start_date is fixed, not computed from the clock", False, + "shifted-clock parse failed: " + proc.tail(20)) + else: + grader.primary("start_date is fixed, not computed from the clock", + shifted["start_date"] == attrs["start_date"], + f"start_date {attrs['start_date']}; with the clock +400 days: {shifted['start_date']}") + + # Backfill from an empty warehouse, exactly as the scheduler would create the runs. + shutil.rmtree(ws / "warehouse", ignore_errors=True) + res, log = execute_runs(py, ws, "backfill") + backfill_ok = False + if res is None: + grader.primary("catchup backfills exactly 09-01..09-29", False, "run probe crashed: " + log) + elif res["n_runs"] > MAX_RUNS: + grader.primary("catchup backfills exactly 09-01..09-29", False, + f"the scheduler would create more than {MAX_RUNS} runs up to {NOW} " + f"(first run {res['first']}); start_date is before the data") + else: + failed = [(r["logical_date"], r["state"]) for r in res["runs"] if r["state"] != "success"] + passed, detail = check_warehouse(read_rows(py, ws), exp) + backfill_ok = not failed and passed and not res.get("aborted") + grader.primary("catchup backfills exactly 09-01..09-29", backfill_ok, + f"{res['n_runs']} runs, first {res['first']}; " + + (f"failed runs {failed[:5]}; " if failed else "") + (res.get("aborted") or "") + detail) + + runs = res.get("runs", []) if isinstance(res, dict) else [] + grader.primary("each run loads a day whose export has already landed", *loads_after_export_lands(runs)) + + # Corrupt REPAIR_DAY, re-run the run that loaded it. + idx = next((r["i"] for r in runs if r.get("new_days") == [REPAIR_DAY]), None) + db = ws / WAREHOUSE_REL + if not backfill_ok or idx is None or not db.exists(): + grader.primary("re-running a day repairs it and keeps other days", False, + f"needs a clean backfill in which one run loaded {REPAIR_DAY} (run index {idx})") + else: + corrupt = g.run_cmd([py, "-c", _CORRUPT_CODE, str(db), REPAIR_DAY], env=g.scrubbed_environ(), timeout=120) + rerun, log = execute_runs(py, ws, "single", idx) if corrupt.ok else (None, corrupt.tail(10)) + states = [r["state"] for r in rerun["runs"]] if rerun else [] + passed, detail = check_warehouse(read_rows(py, ws), exp) + grader.primary("re-running a day repairs it and keeps other days", + states == ["success"] and passed, + f"re-run states {states}; {detail}" if rerun else "re-run failed: " + log) + + # Plain `airflow dags test`. + scratch = g.copy_workspace(ws) + shutil.rmtree(scratch / "warehouse", ignore_errors=True) + manual = g.run_dags_test(py, scratch, DAG_ID, "2026-09-15", env=g.airflow_env(scratch)) + grader.primary("airflow dags test succeeds", manual.ok, + "" if manual.ok else f"failed tasks {manual.failed_tasks()}: {manual.log[-2500:]}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/catchup_old_start_date/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/catchup_old_start_date/dags/pageviews_daily.py new file mode 100644 index 0000000..1273758 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/catchup_old_start_date/dags/pageviews_daily.py @@ -0,0 +1,72 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/date_from_wall_clock/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/date_from_wall_clock/dags/pageviews_daily.py new file mode 100644 index 0000000..1d17689 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/date_from_wall_clock/dags/pageviews_daily.py @@ -0,0 +1,72 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day() -> int: + day = pendulum.now("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/dynamic_start_date/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/dynamic_start_date/dags/pageviews_daily.py new file mode 100644 index 0000000..e575f5c --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/dynamic_start_date/dags/pageviews_daily.py @@ -0,0 +1,73 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + # backfill the last 30 days + start_date=pendulum.today("UTC").subtract(days=28), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/max_active_tasks_only/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/max_active_tasks_only/dags/pageviews_daily.py new file mode 100644 index 0000000..5416761 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/max_active_tasks_only/dags/pageviews_daily.py @@ -0,0 +1,56 @@ +"""Daily page view rollup for the marketing dashboard. + +Uses a data-interval timetable anchored at 02:00 UTC: the run for UTC day D +covers [D 02:00, D+1 02:00) and starts when the interval closes, one hour after +the raw export for D lands. The date of `data_interval_start` is the day to +load. catchup backfills from the first export (2026-09-01); max_active_tasks=1 +keeps a single writer on the DuckDB file across all runs. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +def load_pageviews(day: str) -> int: + """Replace `daily_pageviews` rows for ``day`` (YYYY-MM-DD) from its raw export.""" + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + with duckdb.connect(str(WAREHOUSE)) as con: + con.execute( + "CREATE TABLE IF NOT EXISTS daily_pageviews " + "(view_date DATE, page VARCHAR, views INTEGER, unique_visitors INTEGER)" + ) + con.begin() + con.execute("DELETE FROM daily_pageviews WHERE view_date = ?::DATE", [day]) + con.execute( + "INSERT INTO daily_pageviews " + "SELECT ?::DATE, page, count(*), count(DISTINCT visitor_id) " + "FROM read_csv(?, header = true) GROUP BY page", + [day, str(source)], + ) + con.commit() + return con.execute("SELECT count(*) FROM daily_pageviews WHERE view_date = ?::DATE", [day]).fetchone()[0] + + +with DAG( + dag_id="pageviews_daily", + schedule=CronDataIntervalTimetable("0 2 * * *", timezone="UTC"), + start_date=pendulum.datetime(2026, 9, 1, 2, tz="UTC"), + catchup=True, + max_active_tasks=1, + default_args={"owner": "web-analytics", "retries": 3, "retry_delay": pendulum.duration(hours=1)}, + tags=["web", "warehouse"], +): + PythonOperator( + task_id="load_day", + python_callable=load_pageviews, + op_kwargs={"day": "{{ data_interval_start | ds }}"}, + ) diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/no_concurrency_limit/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/no_concurrency_limit/dags/pageviews_daily.py new file mode 100644 index 0000000..7847924 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/no_concurrency_limit/dags/pageviews_daily.py @@ -0,0 +1,71 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 2, tz="UTC"), + catchup=True, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/same_day_skip_missing/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/same_day_skip_missing/dags/pageviews_daily.py new file mode 100644 index 0000000..1c3b78d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/same_day_skip_missing/dags/pageviews_daily.py @@ -0,0 +1,76 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day of its logical date into `daily_pageviews`, skipping when that day's +export is not there yet. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-01 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task +from airflow.sdk.exceptions import AirflowSkipException + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").to_date_string() + source = RAW_DIR / f"{day}.csv" + if not source.exists(): + raise AirflowSkipException(f"no export for {day} yet") + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/skip_loaded_days/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/skip_loaded_days/dags/pageviews_daily.py new file mode 100644 index 0000000..76f3a87 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/skip_loaded_days/dags/pageviews_daily.py @@ -0,0 +1,77 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + loaded = con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + if loaded: + # already backfilled; never load a day twice + return loaded + con.execute("BEGIN TRANSACTION") + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/mutants/start_date_sep1_previous_day/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/start_date_sep1_previous_day/dags/pageviews_daily.py new file mode 100644 index 0000000..f01eb93 --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/mutants/start_date_sep1_previous_day/dags/pageviews_daily.py @@ -0,0 +1,72 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-backfill-pageviews/reference/dags/pageviews_daily.py b/evals/airflow/cases/scheduling-backfill-pageviews/reference/dags/pageviews_daily.py new file mode 100644 index 0000000..293559d --- /dev/null +++ b/evals/airflow/cases/scheduling-backfill-pageviews/reference/dags/pageviews_daily.py @@ -0,0 +1,72 @@ +"""Daily page view rollup for the marketing dashboard. + +The web tier drops one raw export per UTC day into data/pageviews/.csv +at about 01:00 UTC the next morning. Each run fires at 03:00 UTC and aggregates +the day before its logical date into `daily_pageviews`. + +Backfill: exports start on 2026-09-01, so the first run (2026-09-02 03:00) +loads 2026-09-01 and catchup creates every run since. Runs are serialized +(max_active_runs=1) because DuckDB allows one writer per file, and each run +replaces its own day, so re-runs never double count. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +RAW_DIR = PROJECT_DIR / "data" / "pageviews" +WAREHOUSE = PROJECT_DIR / "warehouse" / "web.duckdb" + + +@dag( + dag_id="pageviews_daily", + schedule="0 3 * * *", + start_date=pendulum.datetime(2026, 9, 2, tz="UTC"), + catchup=True, + max_active_runs=1, + default_args={"owner": "web-analytics", "retries": 1}, + tags=["web", "warehouse"], +) +def pageviews_daily(): + @task + def load_day(logical_date=None) -> int: + day = pendulum.instance(logical_date).in_timezone("UTC").subtract(days=1).to_date_string() + source = RAW_DIR / f"{day}.csv" + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_pageviews ( + view_date DATE, + page VARCHAR, + views INTEGER, + unique_visitors INTEGER + ) + """ + ) + con.execute("BEGIN TRANSACTION") + con.execute("DELETE FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day]) + con.execute( + f""" + INSERT INTO daily_pageviews + SELECT CAST(? AS DATE), page, count(*), count(DISTINCT visitor_id) + FROM read_csv('{source}', header = true) + GROUP BY page + """, + [day], + ) + con.execute("COMMIT") + return con.execute( + "SELECT count(*) FROM daily_pageviews WHERE view_date = CAST(? AS DATE)", [day] + ).fetchone()[0] + finally: + con.close() + + load_day() + + +pageviews_daily() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/alt_reference/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/alt_reference/dags/orders_daily_load.py new file mode 100644 index 0000000..3f52f80 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/alt_reference/dags/orders_daily_load.py @@ -0,0 +1,85 @@ +"""Loads the run's day of shop orders into the local DuckDB warehouse (upsert style).""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +def _connect(): + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR PRIMARY KEY, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE PRIMARY KEY, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + return con + + +def upsert_orders(ds: str) -> None: + """Upsert every non-cancelled order placed on ``ds`` (keyed by order_id).""" + con = _connect() + try: + con.execute( + f""" + INSERT OR REPLACE INTO orders + SELECT order_id, CAST(order_ts AS DATE), customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS DATE) = CAST(? AS DATE) AND status <> 'cancelled' + """, + [ds], + ) + finally: + con.close() + + +def upsert_revenue(ds: str) -> None: + con = _connect() + try: + con.execute( + """ + INSERT OR REPLACE INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders WHERE order_date = CAST(? AS DATE) + GROUP BY order_date + """, + [ds], + ) + finally: + con.close() + + +with DAG( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +): + orders = PythonOperator(task_id="upsert_orders", python_callable=upsert_orders, op_kwargs={"ds": "{{ ds }}"}) + revenue = PythonOperator(task_id="upsert_revenue", python_callable=upsert_revenue, op_kwargs={"ds": "{{ ds }}"}) + orders >> revenue diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/case.yaml b/evals/airflow/cases/scheduling-idempotent-orders-load/case.yaml new file mode 100644 index 0000000..53ce595 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/case.yaml @@ -0,0 +1,34 @@ +id: scheduling-idempotent-orders-load +area: scheduling +split: dev +airflow_version: "2.11" +max_turns: 40 +timeout_s: 900 +prompt: | + Our `orders_daily_load` DAG keeps corrupting the warehouse. When someone + clears a run or re-runs it after a failure, the orders get inserted again and + finance sees doubled revenue in `daily_revenue`. We also want to backfill + early March, but right now every run just loads "yesterday" relative to + whenever it happens to execute, so a backfill would load the same day over + and over. + + Please fix the DAG so each run loads the day it is scheduled for, and + re-running, retrying or backfilling any day is always safe: the day ends up + with exactly the right rows even if a previous attempt died halfway through, + and other days are left alone. Keep the table names and columns the same, + because the dashboard reads them. The export we load from is + data/orders.csv. +description: | + Idempotency and backfill safety for an Airflow 2.11 daily DuckDB load (pain + point P4, plus P2 logical-date semantics). The fixture appends with + datetime.now()-based dates into `orders` and `daily_revenue`. The grader runs + the DAG as the scheduler would (the timetable's own logical date and data + interval) for 2026-03-09 and then 2026-03-10 against an empty warehouse and + checks exact per-day rows. It then re-runs 2026-03-10 (no duplicates, the + previous day untouched), simulates a half-written day (rows missing, stale + amounts, stale rollup) and re-runs again, which must fully repair it. A + plain `airflow dags test` must also succeed. Delete+insert and PK upserts + both pass (alt_reference). Mutants: off-by-one day from + `logical_date - 1 day`, table-wide replace, skip-if-already-loaded, rollup + still appending, an inclusive interval end that double-counts a midnight + order, and insert-only-missing order_ids (never repairs stale amounts). diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/README.md b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/README.md new file mode 100644 index 0000000..366d41e --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/README.md @@ -0,0 +1,16 @@ +# shop-analytics + +Airflow 2.11 project that loads the shop's order exports into a local DuckDB +warehouse (`warehouse/analytics.duckdb`). + +| DAG | What it does | +|---|---| +| `orders_daily_load` | Loads the previous day's orders into `orders`, then refreshes `daily_revenue` | +| `customers_snapshot` | Full refresh of the `customers` dimension | + +Tables read by the finance dashboard (do not change their columns): + +- `orders(order_id, order_date, customer_id, amount_usd, loaded_at)` +- `daily_revenue(order_date, order_count, revenue_usd)` + +Local run: `airflow dags test orders_daily_load 2026-03-10` diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/customers_snapshot.py b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/customers_snapshot.py new file mode 100644 index 0000000..52e7f00 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/customers_snapshot.py @@ -0,0 +1,37 @@ +"""Nightly full snapshot of the customer dimension (small table, full refresh).""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "customers.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="customers_snapshot", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def customers_snapshot(): + @task + def refresh_customers() -> int: + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + con.execute( + f"CREATE OR REPLACE TABLE customers AS SELECT * FROM read_csv_auto('{SOURCE_CSV}')" + ) + count = con.execute("SELECT count(*) FROM customers").fetchone()[0] + con.close() + return count + + refresh_customers() + + +customers_snapshot() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/orders_daily_load.py new file mode 100644 index 0000000..53eba3b --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/dags/orders_daily_load.py @@ -0,0 +1,89 @@ +"""Loads yesterday's shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. This DAG copies the +previous day's non-cancelled orders into `orders` and then refreshes the +`daily_revenue` rollup that the finance dashboard reads. +""" + +from datetime import datetime, timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_orders() -> str: + day = (datetime.now() - timedelta(days=1)).date() + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS DATE) = ? + AND status <> 'cancelled' + """, + [day], + ) + con.close() + return day.isoformat() + + @task + def build_daily_revenue(day: str) -> None: + con = duckdb.connect(str(WAREHOUSE)) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date = CAST(? AS DATE) + GROUP BY order_date + """, + [day], + ) + con.close() + + build_daily_revenue(load_orders()) + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/customers.csv b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/customers.csv new file mode 100644 index 0000000..7e01927 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/customers.csv @@ -0,0 +1,41 @@ +customer_id,name,country,signup_date +C-001,Farah E.,DE,2025-11-02 +C-002,Ben R.,US,2025-06-19 +C-003,Ava Q.,CA,2025-01-03 +C-004,Gus N.,US,2025-04-03 +C-005,Ivan N.,US,2025-10-04 +C-006,Dev U.,US,2025-10-19 +C-007,Gus B.,CA,2025-01-18 +C-008,Chloe J.,DE,2025-03-18 +C-009,Ben S.,GB,2025-09-27 +C-010,Chloe D.,CA,2025-06-04 +C-011,Ivan W.,US,2025-10-02 +C-012,Jun G.,DE,2025-11-18 +C-013,Gus Y.,GB,2025-08-19 +C-014,Hana L.,GB,2025-04-26 +C-015,Chloe W.,CA,2025-02-19 +C-016,Elif Q.,DE,2025-06-24 +C-017,Hana J.,US,2025-02-17 +C-018,Gus F.,GB,2025-03-16 +C-019,Gus B.,US,2025-09-19 +C-020,Farah K.,GB,2025-10-16 +C-021,Jun Z.,DE,2025-02-27 +C-022,Ben I.,DE,2025-12-22 +C-023,Ben B.,GB,2025-11-19 +C-024,Hana J.,DE,2025-11-12 +C-025,Ava O.,GB,2025-03-20 +C-026,Ben P.,US,2025-04-25 +C-027,Elif E.,CA,2025-07-13 +C-028,Hana C.,CA,2025-08-13 +C-029,Ivan I.,CA,2025-07-28 +C-030,Ivan I.,DE,2025-06-22 +C-031,Gus H.,CA,2025-02-06 +C-032,Chloe H.,CA,2025-01-16 +C-033,Jun F.,GB,2025-05-01 +C-034,Chloe N.,GB,2025-10-19 +C-035,Farah E.,US,2025-08-28 +C-036,Ivan M.,DE,2025-07-13 +C-037,Ben P.,DE,2025-01-07 +C-038,Ben G.,DE,2025-03-04 +C-039,Farah T.,US,2025-02-01 +C-040,Jun E.,US,2025-06-20 diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/orders.csv b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/orders.csv new file mode 100644 index 0000000..a09861e --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/data/orders.csv @@ -0,0 +1,131 @@ +order_id,order_ts,customer_id,amount_usd,status +O-100001,2026-03-01 00:24:21,C-017,202.66,shipped +O-100002,2026-03-01 02:43:00,C-006,69.50,shipped +O-100003,2026-03-01 05:52:23,C-014,184.26,shipped +O-100004,2026-03-01 09:24:26,C-012,96.45,paid +O-100005,2026-03-01 19:32:33,C-019,192.17,shipped +O-100006,2026-03-01 23:22:44,C-027,159.33,shipped +O-100007,2026-03-01 23:30:34,C-034,99.48,shipped +O-100008,2026-03-02 00:34:56,C-004,207.01,paid +O-100009,2026-03-02 07:26:13,C-026,72.52,paid +O-100010,2026-03-02 07:47:10,C-036,178.19,cancelled +O-100011,2026-03-02 10:42:00,C-006,163.83,shipped +O-100012,2026-03-02 20:16:00,C-032,144.82,paid +O-100013,2026-03-02 20:45:35,C-020,238.27,cancelled +O-100014,2026-03-02 22:35:36,C-023,12.94,paid +O-100015,2026-03-03 00:10:09,C-030,136.80,shipped +O-100016,2026-03-03 07:23:16,C-004,14.30,paid +O-100017,2026-03-03 07:38:02,C-037,128.37,shipped +O-100018,2026-03-03 08:08:14,C-022,182.23,paid +O-100019,2026-03-03 14:31:09,C-031,8.63,paid +O-100020,2026-03-03 19:25:12,C-010,81.51,shipped +O-100021,2026-03-03 20:11:44,C-013,238.11,paid +O-100022,2026-03-03 20:40:20,C-008,64.55,cancelled +O-100023,2026-03-04 03:24:26,C-014,145.19,paid +O-100024,2026-03-04 03:32:17,C-013,83.61,paid +O-100025,2026-03-04 06:38:51,C-040,14.11,paid +O-100026,2026-03-04 10:13:27,C-020,139.53,shipped +O-100027,2026-03-04 11:58:23,C-034,234.49,shipped +O-100028,2026-03-04 18:03:03,C-003,39.70,paid +O-100029,2026-03-04 21:26:44,C-010,40.72,paid +O-100030,2026-03-04 22:28:58,C-001,186.13,cancelled +O-100031,2026-03-05 00:32:22,C-003,164.71,paid +O-100032,2026-03-05 06:19:59,C-037,24.87,paid +O-100033,2026-03-05 11:22:31,C-035,143.90,paid +O-100034,2026-03-05 13:57:24,C-003,65.42,paid +O-100035,2026-03-05 14:41:34,C-008,36.02,cancelled +O-100036,2026-03-05 18:40:34,C-007,118.10,shipped +O-100037,2026-03-05 23:11:38,C-012,83.13,cancelled +O-100038,2026-03-05 23:28:57,C-005,185.02,paid +O-100039,2026-03-06 07:28:25,C-022,145.28,shipped +O-100040,2026-03-06 12:04:47,C-020,42.80,paid +O-100041,2026-03-06 13:36:30,C-023,240.28,paid +O-100042,2026-03-06 17:22:20,C-029,38.66,shipped +O-100043,2026-03-06 17:43:17,C-037,227.00,paid +O-100044,2026-03-06 20:31:27,C-039,59.21,paid +O-100045,2026-03-07 02:29:27,C-015,67.19,shipped +O-100046,2026-03-07 04:20:25,C-026,38.30,paid +O-100047,2026-03-07 07:52:00,C-021,75.71,paid +O-100048,2026-03-07 08:17:07,C-018,185.18,paid +O-100049,2026-03-07 10:07:17,C-021,85.13,shipped +O-100050,2026-03-07 10:59:51,C-038,77.69,cancelled +O-100051,2026-03-07 15:08:10,C-028,136.26,shipped +O-100052,2026-03-07 16:11:39,C-026,150.25,shipped +O-100053,2026-03-07 18:17:34,C-010,248.94,shipped +O-100054,2026-03-07 21:59:10,C-037,199.44,shipped +O-100055,2026-03-08 02:40:45,C-028,136.78,paid +O-100056,2026-03-08 05:39:48,C-014,215.29,paid +O-100057,2026-03-08 06:49:06,C-036,167.77,shipped +O-100058,2026-03-08 07:48:49,C-007,105.18,paid +O-100059,2026-03-08 09:03:22,C-030,123.76,shipped +O-100060,2026-03-08 10:32:39,C-040,8.96,paid +O-100061,2026-03-08 12:16:20,C-036,68.93,shipped +O-100062,2026-03-08 15:22:41,C-040,107.00,shipped +O-100063,2026-03-09 00:22:51,C-030,97.37,shipped +O-100064,2026-03-09 04:15:56,C-018,34.27,paid +O-100065,2026-03-09 05:17:07,C-014,194.62,cancelled +O-100066,2026-03-09 09:25:53,C-005,219.41,paid +O-100067,2026-03-09 10:41:07,C-013,125.08,paid +O-100068,2026-03-09 18:38:13,C-006,87.45,shipped +O-100069,2026-03-09 23:14:03,C-003,61.79,paid +O-100070,2026-03-09 23:26:24,C-020,163.33,shipped +O-100127,2026-03-09 23:59:59,C-011,77.39,paid +O-100128,2026-03-10 00:00:00,C-032,65.90,paid +O-100071,2026-03-10 06:02:36,C-034,241.50,paid +O-100072,2026-03-10 13:21:42,C-039,64.38,shipped +O-100073,2026-03-10 20:26:57,C-040,215.58,cancelled +O-100074,2026-03-10 21:16:29,C-006,168.96,paid +O-100075,2026-03-10 21:28:34,C-034,62.11,shipped +O-100076,2026-03-10 21:59:15,C-010,102.06,paid +O-100077,2026-03-10 22:38:26,C-002,84.05,paid +O-100078,2026-03-10 22:45:18,C-033,55.07,shipped +O-100079,2026-03-10 22:48:55,C-002,58.36,paid +O-100129,2026-03-10 23:59:59,C-020,97.03,shipped +O-100130,2026-03-11 00:00:00,C-020,109.83,paid +O-100080,2026-03-11 01:19:32,C-027,51.11,shipped +O-100081,2026-03-11 03:05:36,C-013,31.94,paid +O-100082,2026-03-11 05:47:28,C-034,228.51,paid +O-100083,2026-03-11 08:29:05,C-005,149.32,cancelled +O-100084,2026-03-11 10:02:18,C-032,77.74,shipped +O-100085,2026-03-11 13:50:29,C-004,129.10,paid +O-100086,2026-03-11 14:27:53,C-006,222.93,shipped +O-100087,2026-03-11 17:22:25,C-022,86.23,cancelled +O-100088,2026-03-11 22:35:50,C-019,43.52,paid +O-100089,2026-03-12 00:23:24,C-019,131.02,paid +O-100090,2026-03-12 01:36:27,C-012,61.60,paid +O-100091,2026-03-12 05:27:53,C-006,83.54,shipped +O-100092,2026-03-12 08:14:49,C-006,195.39,cancelled +O-100093,2026-03-12 09:10:38,C-015,51.84,shipped +O-100094,2026-03-12 09:59:30,C-036,68.40,shipped +O-100095,2026-03-12 20:21:17,C-037,75.05,shipped +O-100096,2026-03-12 20:23:03,C-013,202.17,shipped +O-100097,2026-03-12 23:47:40,C-017,21.40,shipped +O-100098,2026-03-13 03:29:19,C-002,105.28,paid +O-100099,2026-03-13 12:42:09,C-010,243.12,paid +O-100100,2026-03-13 13:35:53,C-028,80.72,shipped +O-100101,2026-03-13 15:07:46,C-013,71.83,shipped +O-100102,2026-03-13 16:23:39,C-013,63.25,shipped +O-100103,2026-03-13 17:18:50,C-021,19.50,shipped +O-100104,2026-03-13 18:52:41,C-014,58.05,paid +O-100105,2026-03-13 19:51:52,C-019,158.58,paid +O-100106,2026-03-13 21:24:00,C-027,38.63,shipped +O-100107,2026-03-13 23:30:40,C-040,197.97,cancelled +O-100108,2026-03-14 00:24:08,C-005,208.45,paid +O-100109,2026-03-14 00:39:29,C-037,144.33,shipped +O-100110,2026-03-14 03:15:02,C-034,75.25,shipped +O-100111,2026-03-14 07:42:31,C-026,183.13,cancelled +O-100112,2026-03-14 10:51:26,C-025,146.13,paid +O-100113,2026-03-14 12:20:47,C-004,127.16,paid +O-100114,2026-03-14 13:46:34,C-025,224.49,paid +O-100115,2026-03-14 20:00:29,C-006,12.10,paid +O-100116,2026-03-14 20:07:28,C-007,69.96,shipped +O-100117,2026-03-14 22:25:21,C-018,59.14,cancelled +O-100118,2026-03-15 00:29:09,C-016,168.64,shipped +O-100119,2026-03-15 01:56:27,C-030,188.74,shipped +O-100120,2026-03-15 04:51:48,C-032,114.71,cancelled +O-100121,2026-03-15 06:17:25,C-003,21.90,paid +O-100122,2026-03-15 08:33:10,C-014,35.56,shipped +O-100123,2026-03-15 11:01:54,C-017,220.08,cancelled +O-100124,2026-03-15 13:44:59,C-033,229.22,paid +O-100125,2026-03-15 14:57:31,C-016,171.90,paid +O-100126,2026-03-15 17:59:34,C-012,167.69,paid diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/requirements.txt b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/requirements.txt new file mode 100644 index 0000000..e53edc8 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/fixture/requirements.txt @@ -0,0 +1,3 @@ +apache-airflow==2.11.2 +duckdb==1.4.4 +pandas==2.1.4 diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/grade.py b/evals/airflow/cases/scheduling-idempotent-orders-load/grade.py new file mode 100644 index 0000000..0dbb6b2 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/grade.py @@ -0,0 +1,234 @@ +"""Grader for scheduling-idempotent-orders-load (Airflow 2.11). + +The DAG must load exactly the day its run covers, and re-running a day must +converge to the correct rows without touching the neighbouring day. + +Runs are executed as the SCHEDULER would create them: the grader takes the +first scheduled run that starts on/after a given instant from the DAG's own +timetable and executes it with ``dag.test`` using that run's logical date and +data interval. (``airflow dags test`` alone gives manual-run intervals, which +differ from scheduled ones on 2.x.) A plain ``airflow dags test`` is checked +separately. +""" + +from __future__ import annotations + +import csv +import json +import os +import shutil +import sys +from collections import defaultdict +from decimal import Decimal +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "orders_daily_load" +PREV_DAY, DAY = "2026-03-09", "2026-03-10" +WAREHOUSE_REL = Path("warehouse") / "analytics.duckdb" + +# --------------------------------------------------------------------------- +# Scheduled-run simulation (shared pattern across scheduling-* cases) +# --------------------------------------------------------------------------- +_SIM_CODE = r''' +import pendulum +from airflow.timetables.base import DataInterval, TimeRestriction +dag = get_dag(DAG_ID) +tt = core_timetable(dag) +not_before = pendulum.parse(NOT_BEFORE) +earliest = not_before.subtract(days=10) +last, info = None, None +for _ in range(2000): + nxt = tt.next_dagrun_info(last_automated_data_interval=last, + restriction=TimeRestriction(earliest=earliest, latest=None, catchup=True)) + if nxt is None: + break + if nxt.run_after >= not_before: + info = nxt + break + last = nxt.data_interval +if info is None: + raise SystemExit("no scheduled run at or after " + NOT_BEFORE) +di = info.data_interval +logical = info.logical_date +_patched = lambda self, run_after: DataInterval(di.start, di.end) +type(tt).infer_manual_data_interval = _patched +type(dag.timetable).infer_manual_data_interval = _patched +print("SIMRUN", logical, di.start, di.end, info.run_after, flush=True) +params = dag.test.__code__.co_varnames +dr = dag.test(**({"logical_date": logical} if "logical_date" in params else {"execution_date": logical})) +RESULT = {"state": str(getattr(dr.state, "value", dr.state)) if dr is not None else None, + "logical_date": logical.isoformat(), "start": di.start.isoformat(), "end": di.end.isoformat(), + "run_after": info.run_after.isoformat()} +''' + + +def scheduled_run(py: str, ws: Path, dag_id: str, not_before: str) -> tuple[bool, dict | None, str]: + """Execute the first scheduled run of ``dag_id`` with run_after >= ``not_before``. + + Every call uses a fresh metadata DB, so a re-run always really executes.""" + env = g.airflow_env(ws) + db = g.ensure_db(py, env) + if not db.ok: + return False, None, "db migrate failed: " + db.tail(20) + code = f"DAG_ID = {dag_id!r}\nNOT_BEFORE = {not_before!r}\n" + _SIM_CODE + res, proc = g.probe_json(py, code, ws, env=env) + ok = bool(res) and res.get("state") == "success" + return ok, res, proc.tail(40) + + +# --------------------------------------------------------------------------- +# Ground truth and warehouse reads +# --------------------------------------------------------------------------- + + +def expected() -> tuple[dict, dict]: + """(orders per date {date: {order_id: (customer, amount)}}, revenue {date: (count, sum)}).""" + orders: dict[str, dict[str, tuple[str, str]]] = defaultdict(dict) + with (CASE_DIR / "fixture" / "data" / "orders.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["status"] == "cancelled": + continue + orders[row["order_ts"][:10]][row["order_id"]] = (row["customer_id"], f"{Decimal(row['amount_usd']):.2f}") + revenue = {d: (len(o), f"{sum(Decimal(a) for _, a in o.values()):.2f}") for d, o in orders.items()} + return orders, revenue + + +_READ_CODE = r''' +import duckdb, json, sys +con = duckdb.connect(sys.argv[1], read_only=True) +out = {} +out["orders"] = [[str(r[0]), str(r[1]), str(r[2]), f"{float(r[3]):.2f}"] for r in con.execute( + "SELECT order_id, CAST(order_date AS VARCHAR), customer_id, amount_usd FROM orders").fetchall()] +out["revenue"] = [[str(r[0]), int(r[1]), f"{float(r[2]):.2f}"] for r in con.execute( + "SELECT CAST(order_date AS VARCHAR), order_count, revenue_usd FROM daily_revenue").fetchall()] +print("__WH__" + json.dumps(out)) +''' + + +def read_warehouse(py: str, ws: Path) -> tuple[dict | None, str]: + db = ws / WAREHOUSE_REL + if not db.exists(): + return None, f"{WAREHOUSE_REL} does not exist" + res = g.run_cmd([py, "-c", _READ_CODE, str(db)], env=g.scrubbed_environ(), timeout=120) + for line in res.stdout.splitlines(): + if line.startswith("__WH__"): + return json.loads(line[6:]), "" + return None, res.tail(10) + + +def check_days(wh: dict | None, days: list[str], exp_orders: dict, exp_rev: dict) -> tuple[bool, str]: + """Exactly the expected rows for ``days`` (no duplicates) and no rows for any other day.""" + if wh is None: + return False, "warehouse unreadable" + problems = [] + by_day: dict[str, list[list[str]]] = defaultdict(list) + for r in wh["orders"]: + by_day[r[1]].append(r) + rev_by_day: dict[str, list] = defaultdict(list) + for r in wh["revenue"]: + rev_by_day[r[0]].append(r) + extra_days = sorted((set(by_day) | set(rev_by_day)) - set(days)) + if extra_days: + problems.append(f"rows for unexpected days {extra_days}") + for d in days: + rows = by_day.get(d, []) + ids = [r[0] for r in rows] + if len(ids) != len(set(ids)): + problems.append(f"{d}: duplicate order_ids ({len(ids)} rows, {len(set(ids))} distinct)") + got = {r[0]: (r[2], r[3]) for r in rows} + if got != exp_orders.get(d, {}): + missing = sorted(set(exp_orders.get(d, {})) - set(got)) + extra = sorted(set(got) - set(exp_orders.get(d, {}))) + wrong = sorted(k for k in set(got) & set(exp_orders.get(d, {})) if got[k] != exp_orders[d][k]) + problems.append(f"{d}: orders differ (missing {missing[:5]}, unexpected {extra[:5]}, wrong values {wrong[:5]})") + rev = rev_by_day.get(d, []) + want = [d, exp_rev[d][0], exp_rev[d][1]] + if rev != [want]: + problems.append(f"{d}: daily_revenue {rev} != [{want}]") + return not problems, "; ".join(problems) + + +_CORRUPT_CODE = r''' +import duckdb, sys +day = sys.argv[2] +con = duckdb.connect(sys.argv[1]) +ids = [r[0] for r in con.execute( + "SELECT order_id FROM orders WHERE CAST(order_date AS VARCHAR) = ? ORDER BY order_id", [day]).fetchall()] +# A crashed attempt left the day half-written: two orders missing, stale amounts, stale rollup. +for oid in ids[:2]: + con.execute("DELETE FROM orders WHERE order_id = ?", [oid]) +con.execute("UPDATE orders SET amount_usd = amount_usd + 1000 WHERE CAST(order_date AS VARCHAR) = ?", [day]) +con.execute("UPDATE daily_revenue SET order_count = 1, revenue_usd = 1 WHERE CAST(order_date AS VARCHAR) = ?", [day]) +con.close() +print("CORRUPTED", len(ids)) +''' + + +def main() -> None: + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + exp_orders, exp_rev = expected() + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + + if dag is not None: + # 1. Plain `airflow dags test` on a scratch copy (manual-run semantics). + scratch = g.copy_workspace(ws) + shutil.rmtree(scratch / "warehouse", ignore_errors=True) + manual = g.run_dags_test(py, scratch, DAG_ID, DAY, env=g.airflow_env(scratch)) + grader.primary("airflow dags test succeeds", manual.ok, + "" if manual.ok else f"failed tasks {manual.failed_tasks()}: {manual.log[-2500:]}") + + # 2. Scheduled runs for PREV_DAY then DAY against an empty warehouse. + shutil.rmtree(ws / "warehouse", ignore_errors=True) + ok_prev, res_prev, log_prev = scheduled_run(py, ws, DAG_ID, f"{DAY}T00:00:00+00:00") + ok_day, res_day, log_day = scheduled_run(py, ws, DAG_ID, "2026-03-11T00:00:00+00:00") + wh, why = read_warehouse(py, ws) + passed, detail = check_days(wh, [PREV_DAY, DAY], exp_orders, exp_rev) + runs_ok = ok_prev and ok_day + grader.primary( + "scheduled runs load exactly their own day", + runs_ok and passed, + (f"runs: {res_prev}, {res_day}; " if runs_ok else f"run failed: {log_prev if not ok_prev else log_day}; ") + + (detail or why), + ) + + # 3. Plain re-run of DAY: nothing may be duplicated, PREV_DAY untouched. + ok_again, res_again, log_again = scheduled_run(py, ws, DAG_ID, "2026-03-11T00:00:00+00:00") + wh, why = read_warehouse(py, ws) + passed, detail = check_days(wh, [PREV_DAY, DAY], exp_orders, exp_rev) + grader.primary("re-running a day creates no duplicates and keeps the previous day", + runs_ok and ok_again and passed, + (detail or why) if ok_again else f"re-run failed: {log_again}") + + # 4. Simulated partial write of DAY, then re-run: must converge again. + db = ws / WAREHOUSE_REL + corrupt = g.run_cmd([py, "-c", _CORRUPT_CODE, str(db), DAY], env=g.scrubbed_environ(), timeout=120) \ + if db.exists() else None + if corrupt is None or not corrupt.ok: + grader.primary("re-run repairs a partially written day", False, + "could not simulate partial write: " + (corrupt.tail(10) if corrupt else "no warehouse")) + else: + ok_fix, res_fix, log_fix = scheduled_run(py, ws, DAG_ID, "2026-03-11T00:00:00+00:00") + wh, why = read_warehouse(py, ws) + passed, detail = check_days(wh, [PREV_DAY, DAY], exp_orders, exp_rev) + grader.primary("re-run repairs a partially written day", runs_ok and ok_fix and passed, + (detail or why) if ok_fix else f"re-run failed: {log_fix}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/inclusive_interval_end/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/inclusive_interval_end/dags/orders_daily_load.py new file mode 100644 index 0000000..9711c4b --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/inclusive_interval_end/dags/orders_daily_load.py @@ -0,0 +1,104 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + con.execute( + "DELETE FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) <= ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/insert_missing_only/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/insert_missing_only/dags/orders_daily_load.py new file mode 100644 index 0000000..d0346d8 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/insert_missing_only/dags/orders_daily_load.py @@ -0,0 +1,102 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + # orders are keyed by order_id: only insert the ones that are not loaded yet + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + AND order_id NOT IN (SELECT order_id FROM orders) + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/logical_date_minus_one/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/logical_date_minus_one/dags/orders_daily_load.py new file mode 100644 index 0000000..9bdfb54 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/logical_date_minus_one/dags/orders_daily_load.py @@ -0,0 +1,106 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from datetime import timedelta +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(logical_date=None) -> None: + # "yesterday" relative to the run instead of the wall clock + day = (logical_date - timedelta(days=1)).naive().replace(hour=0, minute=0, second=0, microsecond=0) + start, end = day, day + timedelta(days=1) + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + con.execute( + "DELETE FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/rollup_still_appends/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/rollup_still_appends/dags/orders_daily_load.py new file mode 100644 index 0000000..7045b17 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/rollup_still_appends/dags/orders_daily_load.py @@ -0,0 +1,100 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + con.execute( + "DELETE FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/skip_if_already_loaded/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/skip_if_already_loaded/dags/orders_daily_load.py new file mode 100644 index 0000000..c8d8c37 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/skip_if_already_loaded/dags/orders_daily_load.py @@ -0,0 +1,111 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + already = con.execute( + "SELECT count(*) FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ).fetchone()[0] + if already: + con.execute("COMMIT") + return + con.execute( + "DELETE FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/truncate_whole_table/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/truncate_whole_table/dags/orders_daily_load.py new file mode 100644 index 0000000..cd1ee5b --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/mutants/truncate_whole_table/dags/orders_daily_load.py @@ -0,0 +1,102 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + con.execute( + "DELETE FROM orders" + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue" + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-idempotent-orders-load/reference/dags/orders_daily_load.py b/evals/airflow/cases/scheduling-idempotent-orders-load/reference/dags/orders_daily_load.py new file mode 100644 index 0000000..6f83e59 --- /dev/null +++ b/evals/airflow/cases/scheduling-idempotent-orders-load/reference/dags/orders_daily_load.py @@ -0,0 +1,104 @@ +"""Loads one day of shop orders into the local DuckDB warehouse. + +The shop backend exports every order to data/orders.csv. Each run owns the day +covered by its data interval: it replaces that day's rows in `orders` and +`daily_revenue` inside one transaction, so re-runs, retries and backfills +converge to the same result and never touch other days. +""" + +from pathlib import Path + +import duckdb +import pendulum +from airflow.decorators import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +SOURCE_CSV = PROJECT_DIR / "data" / "orders.csv" +WAREHOUSE = PROJECT_DIR / "warehouse" / "analytics.duckdb" + + +@dag( + dag_id="orders_daily_load", + schedule="@daily", + start_date=pendulum.datetime(2026, 3, 1, tz="UTC"), + catchup=False, + default_args={"owner": "data-eng"}, + tags=["shop", "warehouse"], +) +def orders_daily_load(): + @task + def load_day(data_interval_start=None, data_interval_end=None) -> None: + start = data_interval_start.naive() if hasattr(data_interval_start, "naive") else data_interval_start + end = data_interval_end.naive() if hasattr(data_interval_end, "naive") else data_interval_end + WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) + con = duckdb.connect(str(WAREHOUSE)) + try: + con.execute( + """ + CREATE TABLE IF NOT EXISTS orders ( + order_id VARCHAR, + order_date DATE, + customer_id VARCHAR, + amount_usd DECIMAL(12, 2), + loaded_at TIMESTAMP + ) + """ + ) + con.execute( + """ + CREATE TABLE IF NOT EXISTS daily_revenue ( + order_date DATE, + order_count INTEGER, + revenue_usd DECIMAL(14, 2) + ) + """ + ) + con.execute("BEGIN TRANSACTION") + except Exception: + con.close() + raise + try: + con.execute( + "DELETE FROM orders WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + f""" + INSERT INTO orders + SELECT order_id, + CAST(order_ts AS DATE), + customer_id, + CAST(amount_usd AS DECIMAL(12, 2)), + now() + FROM read_csv_auto('{SOURCE_CSV}') + WHERE CAST(order_ts AS TIMESTAMP) >= ? + AND CAST(order_ts AS TIMESTAMP) < ? + AND status <> 'cancelled' + """, + [start, end], + ) + con.execute( + "DELETE FROM daily_revenue WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE)", + [start, end], + ) + con.execute( + """ + INSERT INTO daily_revenue + SELECT order_date, count(*), sum(amount_usd) + FROM orders + WHERE order_date >= CAST(? AS DATE) AND order_date < CAST(? AS DATE) + GROUP BY order_date + """, + [start, end], + ) + con.execute("COMMIT") + except Exception: + con.execute("ROLLBACK") + raise + finally: + con.close() + + load_day() + + +orders_daily_load() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/alt_reference/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/alt_reference/dags/fx_pnl_report.py new file mode 100644 index 0000000..19923c1 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/alt_reference/dags/fx_pnl_report.py @@ -0,0 +1,55 @@ +"""Daily FX P&L report for the trading desks. + +Weekday 06:00 New York schedule with data intervals: each run's interval spans +from the previous weekday 06:00 to this one, so the interval start is the +business day being reported (Monday's interval starts on Friday). +""" + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG, CronDataIntervalTimetable + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" + + +def write_pnl_report(trade_date: str) -> str: + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] == trade_date: + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return str(out) + + +with DAG( + dag_id="fx_pnl_report", + schedule=CronDataIntervalTimetable("0 6 * * MON-FRI", timezone="America/New_York"), + start_date=pendulum.datetime(2026, 1, 1, 6, tz="America/New_York"), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +): + PythonOperator( + task_id="build_report", + python_callable=write_pnl_report, + op_kwargs={ + "trade_date": "{{ data_interval_start.in_timezone('America/New_York').strftime('%Y-%m-%d') }}", + }, + ) diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/case.yaml b/evals/airflow/cases/scheduling-weekday-ny-business-day/case.yaml new file mode 100644 index 0000000..8eb6291 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/case.yaml @@ -0,0 +1,34 @@ +id: scheduling-weekday-ny-business-day +area: scheduling +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +prompt: | + The treasury desk wants the `fx_pnl_report` DAG changed. Right now it runs at + midnight UTC every day, including weekends, and reports that same date. What + they need: it should run every weekday at 6am New York time, and each run + should report the previous business day. So Monday morning's report covers + Friday's trades, Tuesday's covers Monday, and so on. Don't worry about bank + holidays for now. The report file name and columns have to stay the same, + because the desk heads pull them into Excel. +description: | + Timezone-aware weekday scheduling and "previous business day" semantics on + Airflow 3.3 (pain points P2 logical-date/data-interval semantics and DST + handling; research 05 section 3 row 5: a bare cron string is a + CronTriggerTimetable with a zero-width interval). The fixture runs @daily in + UTC and reports `ds`. The grader reads the DAG's own timetable around both + 2026 US DST switches (08 Mar and 01 Nov) and requires runs at exactly 06:00 + New York on weekdays only. It then executes the scheduled runs for Monday + 09 Mar (first run after DST starts), Tuesday 10 Mar and Monday 02 Nov (first + run after DST ends), each with the run's own logical date and data interval, + and checks that exactly the expected report (Friday, Monday, Friday) is + written with the right per-desk totals. A plain `airflow dags test` must + also pass. CronTriggerTimetable + weekday arithmetic (reference) and + CronDataIntervalTimetable + data_interval_start (alt_reference) both pass. + A bare cron string with a New York start_date also passes. Mutants: a fixed + 11:00 UTC cron (wrong after DST starts), calendar "yesterday" (Monday + reports Sunday), a bare cron string reading data_interval_start (reports + the run day itself under 3.x trigger semantics), a bare cron string with a + UTC start_date (runs at 06:00 UTC), and the run day taken from the wall + clock instead of the logical date. diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/README.md b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/README.md new file mode 100644 index 0000000..dba9efd --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/README.md @@ -0,0 +1,11 @@ +# treasury-reports + +Airflow 3.3 project for the treasury desk reports. + +| DAG | Output | +|---|---| +| `fx_pnl_report` | `reports/fx_pnl_.csv`, per-desk trade count, notional and P&L | +| `desk_reference_refresh` | `reports/reference/desks.csv` | + +`data/fx_trades.csv` is the nightly export of booked trades. `trade_date` is +the New York trading day the trade was booked on. diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/desk_reference_refresh.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/desk_reference_refresh.py new file mode 100644 index 0000000..b846230 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/desk_reference_refresh.py @@ -0,0 +1,31 @@ +"""Weekly copy of the desk reference list used by the reporting DAGs.""" + +import shutil +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] + + +@dag( + dag_id="desk_reference_refresh", + schedule="@weekly", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reference"], +) +def desk_reference_refresh(): + @task + def copy_desks() -> str: + target = PROJECT_DIR / "reports" / "reference" / "desks.csv" + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(PROJECT_DIR / "data" / "desks.csv", target) + return str(target) + + copy_desks() + + +desk_reference_refresh() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/fx_pnl_report.py new file mode 100644 index 0000000..6008045 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/dags/fx_pnl_report.py @@ -0,0 +1,58 @@ +"""Daily FX P&L report for the trading desks. + +Reads the booked trades export (data/fx_trades.csv, one row per trade with the +New York trade date) and writes one CSV per trading day to +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + schedule="@daily", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(ds=None) -> str: + return str(write_pnl_report(ds)) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/desks.csv b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/desks.csv new file mode 100644 index 0000000..f721a11 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/desks.csv @@ -0,0 +1,4 @@ +desk,region,head +AMER,Americas,New York +EMEA,Europe,London +APAC,Asia Pacific,Singapore diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/fx_trades.csv b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/fx_trades.csv new file mode 100644 index 0000000..1ae1135 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/data/fx_trades.csv @@ -0,0 +1,208 @@ +trade_id,trade_date,desk,ccy_pair,notional_usd,pnl_usd +T50001,2026-02-23,AMER,GBPUSD,4300000.00,-3289.87 +T50002,2026-02-23,EMEA,USDJPY,3000000.00,-1266.63 +T50003,2026-02-23,AMER,USDJPY,4200000.00,-9914.06 +T50004,2026-02-23,EMEA,USDCAD,2200000.00,-20141.12 +T50005,2026-02-23,EMEA,EURUSD,2700000.00,1921.72 +T50006,2026-02-23,AMER,USDCAD,1400000.00,261.62 +T50007,2026-02-23,AMER,AUDUSD,1400000.00,-17812.67 +T50008,2026-02-23,EMEA,USDCAD,4000000.00,7006.65 +T50009,2026-02-23,APAC,USDJPY,4100000.00,19402.98 +T50010,2026-02-24,AMER,EURUSD,1200000.00,1705.06 +T50011,2026-02-24,AMER,GBPUSD,2400000.00,-21067.82 +T50012,2026-02-24,EMEA,EURUSD,400000.00,-8780.54 +T50013,2026-02-24,EMEA,USDJPY,2100000.00,-7838.35 +T50014,2026-02-24,AMER,USDCAD,1300000.00,-9146.59 +T50015,2026-02-24,APAC,USDCAD,4100000.00,15830.77 +T50016,2026-02-24,EMEA,AUDUSD,200000.00,-23949.86 +T50017,2026-02-24,AMER,USDCAD,1100000.00,11491.84 +T50018,2026-02-25,AMER,AUDUSD,2000000.00,-8192.92 +T50019,2026-02-25,AMER,USDCAD,3500000.00,13282.00 +T50020,2026-02-25,AMER,USDJPY,4700000.00,13285.70 +T50021,2026-02-25,AMER,USDCAD,5000000.00,9969.11 +T50022,2026-02-25,AMER,EURUSD,1900000.00,24899.43 +T50023,2026-02-25,APAC,GBPUSD,3000000.00,-5441.02 +T50024,2026-02-25,EMEA,USDCAD,1600000.00,-4118.12 +T50025,2026-02-25,AMER,AUDUSD,3900000.00,-15851.92 +T50026,2026-02-26,APAC,USDCAD,4600000.00,20565.24 +T50027,2026-02-26,EMEA,AUDUSD,500000.00,-14285.84 +T50028,2026-02-26,APAC,AUDUSD,2500000.00,-17222.17 +T50029,2026-02-26,EMEA,GBPUSD,4300000.00,24181.26 +T50030,2026-02-26,AMER,EURUSD,100000.00,-13344.48 +T50031,2026-02-26,APAC,AUDUSD,4000000.00,-21796.50 +T50032,2026-02-26,AMER,USDCAD,400000.00,-1870.89 +T50033,2026-02-27,APAC,EURUSD,500000.00,6081.60 +T50034,2026-02-27,EMEA,EURUSD,700000.00,11278.13 +T50035,2026-02-27,APAC,USDCAD,300000.00,14985.43 +T50036,2026-02-27,EMEA,AUDUSD,4600000.00,16319.88 +T50037,2026-02-27,EMEA,AUDUSD,2800000.00,12170.08 +T50038,2026-03-02,EMEA,USDJPY,3700000.00,1095.97 +T50039,2026-03-02,AMER,USDJPY,4900000.00,18686.92 +T50040,2026-03-02,APAC,USDJPY,3700000.00,16740.94 +T50041,2026-03-02,AMER,GBPUSD,4900000.00,-22649.01 +T50042,2026-03-02,EMEA,GBPUSD,3800000.00,-17.43 +T50043,2026-03-02,AMER,EURUSD,5000000.00,4580.00 +T50044,2026-03-02,EMEA,USDCAD,3000000.00,16254.40 +T50045,2026-03-02,EMEA,USDCAD,2300000.00,-10847.51 +T50046,2026-03-02,EMEA,AUDUSD,1500000.00,6505.08 +T50047,2026-03-03,EMEA,EURUSD,3100000.00,24224.05 +T50048,2026-03-03,APAC,USDCAD,300000.00,-13023.36 +T50049,2026-03-03,AMER,USDJPY,3000000.00,-9095.30 +T50050,2026-03-03,APAC,USDCAD,3300000.00,20958.81 +T50051,2026-03-03,AMER,AUDUSD,300000.00,-15062.40 +T50052,2026-03-03,APAC,AUDUSD,3200000.00,24686.12 +T50053,2026-03-03,AMER,USDJPY,2100000.00,7435.26 +T50054,2026-03-03,AMER,GBPUSD,700000.00,3006.27 +T50055,2026-03-03,EMEA,EURUSD,2700000.00,-7247.19 +T50056,2026-03-04,EMEA,USDJPY,500000.00,1349.47 +T50057,2026-03-04,EMEA,USDCAD,4600000.00,-1418.79 +T50058,2026-03-04,EMEA,EURUSD,3100000.00,-3868.91 +T50059,2026-03-04,APAC,AUDUSD,3600000.00,15282.74 +T50060,2026-03-04,APAC,EURUSD,1200000.00,-12560.16 +T50061,2026-03-04,APAC,AUDUSD,2600000.00,-1233.15 +T50062,2026-03-04,APAC,USDCAD,3000000.00,-12609.82 +T50063,2026-03-04,AMER,GBPUSD,1900000.00,23733.48 +T50064,2026-03-05,AMER,AUDUSD,2200000.00,-24345.56 +T50065,2026-03-05,EMEA,AUDUSD,1800000.00,-17645.70 +T50066,2026-03-05,EMEA,AUDUSD,4400000.00,21029.93 +T50067,2026-03-05,APAC,AUDUSD,1400000.00,9609.81 +T50068,2026-03-05,AMER,EURUSD,100000.00,2114.38 +T50069,2026-03-05,AMER,AUDUSD,2200000.00,9254.00 +T50070,2026-03-05,APAC,USDCAD,3300000.00,4242.51 +T50071,2026-03-05,EMEA,AUDUSD,1400000.00,19023.02 +T50072,2026-03-06,APAC,GBPUSD,2100000.00,-15262.38 +T50073,2026-03-06,AMER,USDJPY,900000.00,-6128.56 +T50074,2026-03-06,EMEA,USDCAD,500000.00,-17133.76 +T50075,2026-03-06,EMEA,EURUSD,600000.00,15598.62 +T50076,2026-03-06,EMEA,AUDUSD,3000000.00,-18231.79 +T50077,2026-03-06,APAC,USDCAD,2800000.00,-10398.13 +T50078,2026-03-06,APAC,USDJPY,2000000.00,7281.97 +T50079,2026-03-09,AMER,USDJPY,200000.00,13593.26 +T50080,2026-03-09,EMEA,USDCAD,1400000.00,-24307.94 +T50081,2026-03-09,APAC,GBPUSD,1100000.00,-23039.35 +T50082,2026-03-09,APAC,EURUSD,1600000.00,-14771.35 +T50083,2026-03-09,APAC,EURUSD,3900000.00,9769.83 +T50084,2026-03-09,EMEA,GBPUSD,2100000.00,22772.90 +T50085,2026-03-09,AMER,GBPUSD,500000.00,-10622.82 +T50086,2026-03-09,EMEA,AUDUSD,2200000.00,6213.81 +T50087,2026-03-10,EMEA,USDJPY,2600000.00,-23524.82 +T50088,2026-03-10,EMEA,USDJPY,4800000.00,7537.69 +T50089,2026-03-10,APAC,USDJPY,3000000.00,-14484.79 +T50090,2026-03-10,AMER,USDJPY,4600000.00,-10316.86 +T50091,2026-03-10,EMEA,USDCAD,4700000.00,17697.43 +T50092,2026-03-11,AMER,EURUSD,3000000.00,1973.11 +T50093,2026-03-11,EMEA,AUDUSD,4200000.00,23848.58 +T50094,2026-03-11,AMER,USDCAD,3600000.00,7730.52 +T50095,2026-03-11,AMER,EURUSD,4700000.00,1125.74 +T50096,2026-03-11,APAC,USDJPY,3100000.00,-174.79 +T50097,2026-03-12,AMER,GBPUSD,300000.00,-11905.61 +T50098,2026-03-12,EMEA,USDJPY,3600000.00,-24496.17 +T50099,2026-03-12,APAC,AUDUSD,2800000.00,-6978.50 +T50100,2026-03-12,APAC,GBPUSD,1800000.00,-3679.67 +T50101,2026-03-12,APAC,USDCAD,2300000.00,-13470.18 +T50102,2026-03-12,AMER,USDCAD,1700000.00,-23713.93 +T50103,2026-03-12,AMER,USDCAD,1200000.00,-11982.99 +T50104,2026-03-12,APAC,EURUSD,2100000.00,-17359.28 +T50105,2026-03-13,APAC,USDCAD,3300000.00,20195.96 +T50106,2026-03-13,APAC,AUDUSD,1100000.00,1292.46 +T50107,2026-03-13,AMER,AUDUSD,3900000.00,5744.21 +T50108,2026-03-13,APAC,USDJPY,3100000.00,-15263.29 +T50109,2026-03-13,APAC,GBPUSD,700000.00,-7474.28 +T50110,2026-03-13,EMEA,USDCAD,3900000.00,-21436.47 +T50111,2026-10-19,APAC,AUDUSD,3300000.00,-1260.01 +T50112,2026-10-19,AMER,EURUSD,4200000.00,9867.69 +T50113,2026-10-19,APAC,USDJPY,2400000.00,-22663.20 +T50114,2026-10-19,APAC,GBPUSD,1300000.00,-3078.66 +T50115,2026-10-19,APAC,EURUSD,4200000.00,-8281.73 +T50116,2026-10-19,AMER,USDCAD,1600000.00,14568.41 +T50117,2026-10-19,EMEA,USDCAD,3400000.00,15716.35 +T50118,2026-10-20,APAC,AUDUSD,2600000.00,5286.63 +T50119,2026-10-20,APAC,EURUSD,4800000.00,-13025.93 +T50120,2026-10-20,EMEA,USDCAD,2100000.00,10704.56 +T50121,2026-10-20,APAC,USDCAD,4600000.00,-8280.32 +T50122,2026-10-20,AMER,USDCAD,5000000.00,-23319.92 +T50123,2026-10-20,EMEA,USDCAD,4200000.00,24072.38 +T50124,2026-10-21,AMER,GBPUSD,4400000.00,7671.68 +T50125,2026-10-21,AMER,GBPUSD,3500000.00,-5364.66 +T50126,2026-10-21,AMER,EURUSD,200000.00,-14113.46 +T50127,2026-10-21,AMER,USDJPY,3900000.00,12636.56 +T50128,2026-10-21,APAC,USDJPY,2100000.00,1275.34 +T50129,2026-10-21,APAC,GBPUSD,300000.00,-5047.21 +T50130,2026-10-21,EMEA,USDJPY,1000000.00,-9729.39 +T50131,2026-10-21,APAC,EURUSD,4300000.00,6229.64 +T50132,2026-10-22,EMEA,USDJPY,4000000.00,4276.70 +T50133,2026-10-22,AMER,EURUSD,1200000.00,15549.77 +T50134,2026-10-22,AMER,USDJPY,800000.00,-19858.27 +T50135,2026-10-22,APAC,GBPUSD,300000.00,5292.02 +T50136,2026-10-22,APAC,EURUSD,5000000.00,2230.49 +T50137,2026-10-23,EMEA,EURUSD,2400000.00,-3218.73 +T50138,2026-10-23,APAC,USDCAD,1100000.00,7290.79 +T50139,2026-10-23,AMER,USDJPY,3100000.00,2474.03 +T50140,2026-10-23,EMEA,AUDUSD,3300000.00,-23724.75 +T50141,2026-10-23,AMER,USDCAD,3400000.00,-15580.39 +T50142,2026-10-26,AMER,USDCAD,5000000.00,9722.54 +T50143,2026-10-26,AMER,USDJPY,3300000.00,-20189.78 +T50144,2026-10-26,AMER,EURUSD,200000.00,4317.10 +T50145,2026-10-26,AMER,GBPUSD,2100000.00,3023.50 +T50146,2026-10-27,AMER,USDCAD,4900000.00,17753.24 +T50147,2026-10-27,EMEA,GBPUSD,3700000.00,8913.09 +T50148,2026-10-27,APAC,USDCAD,3500000.00,-9710.60 +T50149,2026-10-27,APAC,EURUSD,1300000.00,-15838.84 +T50150,2026-10-27,EMEA,EURUSD,1000000.00,-3777.04 +T50151,2026-10-27,EMEA,EURUSD,2100000.00,-6663.26 +T50152,2026-10-27,EMEA,AUDUSD,4900000.00,-14657.92 +T50153,2026-10-28,AMER,USDJPY,700000.00,593.58 +T50154,2026-10-28,EMEA,AUDUSD,4900000.00,-17650.14 +T50155,2026-10-28,AMER,USDCAD,1300000.00,-23324.76 +T50156,2026-10-28,EMEA,EURUSD,3300000.00,22240.13 +T50157,2026-10-28,EMEA,AUDUSD,2400000.00,2118.66 +T50158,2026-10-28,APAC,USDCAD,4600000.00,-16622.16 +T50159,2026-10-28,AMER,USDCAD,3400000.00,-1501.73 +T50160,2026-10-28,APAC,USDJPY,4900000.00,-18449.34 +T50161,2026-10-29,APAC,AUDUSD,1900000.00,7441.04 +T50162,2026-10-29,APAC,AUDUSD,4400000.00,-18465.78 +T50163,2026-10-29,APAC,USDJPY,2500000.00,-8694.52 +T50164,2026-10-29,EMEA,AUDUSD,200000.00,-1323.32 +T50165,2026-10-29,AMER,EURUSD,4100000.00,23124.21 +T50166,2026-10-29,EMEA,USDCAD,700000.00,-24653.28 +T50167,2026-10-29,APAC,AUDUSD,2200000.00,-24355.13 +T50168,2026-10-29,AMER,AUDUSD,2300000.00,-22675.02 +T50169,2026-10-29,APAC,GBPUSD,200000.00,11386.46 +T50170,2026-10-30,EMEA,GBPUSD,800000.00,21596.65 +T50171,2026-10-30,APAC,USDJPY,1200000.00,-5329.21 +T50172,2026-10-30,APAC,EURUSD,1600000.00,6000.31 +T50173,2026-10-30,APAC,EURUSD,5000000.00,-14803.13 +T50174,2026-10-30,APAC,GBPUSD,1600000.00,-17796.86 +T50175,2026-11-02,EMEA,USDCAD,4600000.00,-16417.10 +T50176,2026-11-02,APAC,GBPUSD,4000000.00,-23486.70 +T50177,2026-11-02,AMER,GBPUSD,4600000.00,5261.61 +T50178,2026-11-02,APAC,USDCAD,400000.00,24547.00 +T50179,2026-11-02,AMER,USDJPY,700000.00,-6935.92 +T50180,2026-11-02,AMER,EURUSD,2400000.00,21608.93 +T50181,2026-11-03,AMER,USDCAD,1200000.00,2963.64 +T50182,2026-11-03,AMER,AUDUSD,1300000.00,-20676.21 +T50183,2026-11-03,AMER,EURUSD,2100000.00,15695.38 +T50184,2026-11-03,AMER,EURUSD,4000000.00,-7039.40 +T50185,2026-11-04,APAC,EURUSD,1500000.00,24916.57 +T50186,2026-11-04,EMEA,USDCAD,1100000.00,2337.22 +T50187,2026-11-04,APAC,AUDUSD,4200000.00,4196.65 +T50188,2026-11-04,EMEA,AUDUSD,1500000.00,19222.35 +T50189,2026-11-04,EMEA,EURUSD,4000000.00,20289.46 +T50190,2026-11-04,AMER,USDJPY,3900000.00,6587.50 +T50191,2026-11-04,EMEA,USDCAD,1300000.00,21474.19 +T50192,2026-11-04,EMEA,USDJPY,200000.00,11181.41 +T50193,2026-11-05,EMEA,GBPUSD,3800000.00,19562.22 +T50194,2026-11-05,APAC,EURUSD,4700000.00,12632.19 +T50195,2026-11-05,APAC,USDJPY,4400000.00,7896.18 +T50196,2026-11-05,AMER,AUDUSD,1300000.00,-7058.11 +T50197,2026-11-05,EMEA,GBPUSD,4100000.00,3092.56 +T50198,2026-11-05,AMER,AUDUSD,3900000.00,-290.73 +T50199,2026-11-05,AMER,EURUSD,3400000.00,6551.93 +T50200,2026-11-05,EMEA,USDCAD,4200000.00,5123.33 +T50201,2026-11-05,APAC,USDCAD,1100000.00,-23322.53 +T50202,2026-11-06,EMEA,AUDUSD,3300000.00,20542.65 +T50203,2026-11-06,EMEA,AUDUSD,2500000.00,-20528.58 +T50204,2026-11-06,APAC,EURUSD,2700000.00,9406.90 +T50205,2026-11-06,EMEA,USDCAD,4800000.00,-5567.80 +T50206,2026-11-06,EMEA,USDJPY,4600000.00,11375.26 +T50207,2026-11-06,EMEA,USDCAD,3500000.00,-9711.94 diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/requirements.txt b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/requirements.txt new file mode 100644 index 0000000..b1dd561 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==3.3.2 diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/grade.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/grade.py new file mode 100644 index 0000000..609e7d9 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/grade.py @@ -0,0 +1,233 @@ +"""Grader for scheduling-weekday-ny-business-day (Airflow 3.3). + +1. Schedule: the DAG's own timetable must produce runs at exactly 06:00 + America/New_York on weekdays only, across both 2026 US DST switches. +2. Semantics: the scheduled runs for Mon 09 Mar, Tue 10 Mar and Mon 02 Nov are + executed the way the scheduler would create them (the timetable's logical + date and data interval, via ``dag.test``) and must write exactly the report + for the previous business day, with correct per-desk totals. +3. A plain ``airflow dags test`` succeeds. +""" + +from __future__ import annotations + +import csv +import io +import os +import sys +from collections import defaultdict +from datetime import date, datetime, timedelta, timezone +from pathlib import Path +from zoneinfo import ZoneInfo + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +DAG_ID = "fx_pnl_report" +NY = ZoneInfo("America/New_York") +# Windows around DST start (Sun 2026-03-08) and DST end (Sun 2026-11-01), NY dates [start, end). +WINDOWS = [(date(2026, 3, 4), date(2026, 3, 12)), (date(2026, 10, 28), date(2026, 11, 5))] +# Run day (NY) -> business day its report must cover. +RUN_SCENARIOS = [ + (date(2026, 3, 9), date(2026, 3, 6), "Monday run after DST starts reports Friday"), + (date(2026, 3, 10), date(2026, 3, 9), "Tuesday run reports Monday"), + (date(2026, 11, 2), date(2026, 10, 30), "Monday run after DST ends reports Friday"), +] + + +def ny_midnight_utc(d: date) -> str: + return datetime(d.year, d.month, d.day, tzinfo=NY).astimezone(timezone.utc).isoformat() + + +def expected_run_afters(start: date, end: date) -> list[str]: + out, d = [], start + while d < end: + if d.weekday() < 5: + out.append(datetime(d.year, d.month, d.day, 6, tzinfo=NY).astimezone(timezone.utc).isoformat()) + d += timedelta(days=1) + return out + + +def expected_report(trade_date: str) -> dict[str, tuple[int, float, float]]: + totals: dict[str, list] = defaultdict(lambda: [0, 0.0, 0.0]) + with (CASE_DIR / "fixture" / "data" / "fx_trades.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] == trade_date: + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + return {k: (v[0], round(v[1], 2), round(v[2], 2)) for k, v in totals.items()} + + +def compare_report(text: str, want: dict) -> str: + """'' when the CSV text matches ``want`` (numeric tolerance), else a reason.""" + rows = list(csv.reader(io.StringIO(text))) + if not rows or [c.strip() for c in rows[0]] != ["desk", "trades", "notional_usd", "pnl_usd"]: + return f"unexpected header {rows[:1]}" + got = {} + try: + for r in rows[1:]: + if not r: + continue + got[r[0]] = (int(float(r[1])), float(r[2]), float(r[3])) + except (ValueError, IndexError) as exc: + return f"unparseable row: {exc}" + if set(got) != set(want): + return f"desks {sorted(got)} != {sorted(want)}" + bad = [k for k in want if got[k][0] != want[k][0] or abs(got[k][1] - want[k][1]) > 0.01 + or abs(got[k][2] - want[k][2]) > 0.01] + return f"wrong totals for {bad}: got {[got[k] for k in bad]}, want {[want[k] for k in bad]}" if bad else "" + + +# --------------------------------------------------------------------------- +# Probes +# --------------------------------------------------------------------------- + +_SCHEDULE_CODE = r''' +import pendulum +from airflow.timetables.base import TimeRestriction +dag = get_dag(DAG_ID) +tt = core_timetable(dag) +out = [] +for start, end in WINDOWS: + lo, hi = pendulum.parse(start), pendulum.parse(end) + last, runs = None, [] + for _ in range(500): + info = tt.next_dagrun_info(last_automated_data_interval=last, + restriction=TimeRestriction(earliest=lo.subtract(days=7), latest=None, catchup=True)) + if info is None or info.run_after >= hi: + break + last = info.data_interval + if info.run_after >= lo: + runs.append(pendulum.instance(info.run_after).in_timezone("UTC").isoformat()) + out.append(runs) +RESULT = {"runs": out, "timetable": type(dag.timetable).__name__} +''' + +# Executes the scheduled run whose run_after falls on each NY day in RUN_DAYS, +# with that run's own logical date and data interval, and records the reports +# it wrote (the reports dir is emptied before every run). +_RUNS_CODE = r''' +import pathlib, shutil, pendulum +from airflow.timetables.base import DataInterval, TimeRestriction +dag = get_dag(DAG_ID) +tt = core_timetable(dag) +reports = pathlib.Path("reports") +floor = pendulum.datetime(2026, 1, 1, tz="UTC") +if dag.start_date is not None and dag.start_date > floor: + # A later start_date is not wrong for this request; let the historical runs execute. + dag.start_date = floor + for t in dag.tasks: + if t.start_date is not None and t.start_date > floor: + t.start_date = floor +out = [] +for run_day, lo, hi in RUN_DAYS: + lo, hi = pendulum.parse(lo), pendulum.parse(hi) + last, info = None, None + for _ in range(500): + nxt = tt.next_dagrun_info(last_automated_data_interval=last, + restriction=TimeRestriction(earliest=lo.subtract(days=7), latest=None, catchup=True)) + if nxt is None or nxt.run_after >= hi: + break + if nxt.run_after >= lo: + info = nxt + break + last = nxt.data_interval + if info is None: + out.append({"run_day": run_day, "error": "no scheduled run on this New York day"}) + continue + di = info.data_interval + patched = lambda self, run_after, di=di: DataInterval(di.start, di.end) + type(tt).infer_manual_data_interval = patched + type(dag.timetable).infer_manual_data_interval = patched + shutil.rmtree(reports, ignore_errors=True) + rec = {"run_day": run_day, "logical_date": str(info.logical_date), "interval": [str(di.start), str(di.end)], + "run_after": str(info.run_after)} + try: + dr = dag.test(logical_date=info.logical_date) + rec["state"] = str(getattr(dr.state, "value", dr.state)) if dr is not None else None + tis = dr.get_task_instances() if dr is not None else [] + rec["tasks"] = {ti.task_id: str(getattr(ti.state, "value", ti.state)) for ti in tis} + except Exception as exc: + rec["state"] = "error: " + repr(exc)[:500] + rec["files"] = {p.name: p.read_text() for p in sorted(reports.glob("*.csv"))} if reports.exists() else {} + out.append(rec) +shutil.rmtree(reports, ignore_errors=True) +RESULT = out +''' + + +def main() -> None: + args = g.parse_args() + ws, py = Path(args.workspace), sys.executable + grader = g.Grader() + events = g.load_events(args.events) + + imp = g.import_dags(py, ws) + dag = imp["dags"].get(DAG_ID) + grader.primary("DAGs import cleanly", imp["ok"] and not imp["import_errors"], + imp["probe_error"] or "; ".join(f"{k}: {v.strip().splitlines()[-1]}" + for k, v in imp["import_errors"].items())) + grader.primary(f"DAG {DAG_ID} exists", dag is not None, f"found: {sorted(imp['dags'])}") + + if dag is not None: + # 1. Schedule across both DST switches. + windows = [(ny_midnight_utc(a), ny_midnight_utc(b)) for a, b in WINDOWS] + res, proc = g.probe_json(py, f"DAG_ID = {DAG_ID!r}\nWINDOWS = {windows!r}\n" + _SCHEDULE_CODE, ws) + want = [expected_run_afters(a, b) for a, b in WINDOWS] + if res is None: + grader.primary("runs at 06:00 New York on weekdays only, across DST", False, + "timetable probe failed: " + proc.tail(20)) + else: + got = res["runs"] + grader.primary("runs at 06:00 New York on weekdays only, across DST", got == want, + f"timetable {res['timetable']}; run_after (UTC) got {got}, want {want}") + + # 2. Scheduled runs report the previous business day. + run_days = [(d.isoformat(), ny_midnight_utc(d), ny_midnight_utc(d + timedelta(days=1))) + for d, _, _ in RUN_SCENARIOS] + scratch = g.copy_workspace(ws) + runs, proc = g.probe_json(py, f"DAG_ID = {DAG_ID!r}\nRUN_DAYS = {run_days!r}\n" + _RUNS_CODE, + scratch, env=_migrated_env(py, scratch)) + by_day = {r["run_day"]: r for r in runs} if isinstance(runs, list) else {} + for run_day, report_day, label in RUN_SCENARIOS: + rec = by_day.get(run_day.isoformat()) + if rec is None: + grader.primary(label, False, "run probe failed: " + proc.tail(30)) + continue + if "error" in rec: + grader.primary(label, False, rec["error"]) + continue + name = f"fx_pnl_{report_day.isoformat()}.csv" + problems = [] + if rec.get("state") != "success": + problems.append(f"run state {rec.get('state')} tasks {rec.get('tasks')}") + if sorted(rec["files"]) != [name]: + problems.append(f"reports written {sorted(rec['files'])}, want [{name}]") + elif (why := compare_report(rec["files"][name], expected_report(report_day.isoformat()))): + problems.append(why) + grader.primary(label, not problems, + f"logical_date {rec['logical_date']} interval {rec['interval']}; " + "; ".join(problems)) + + # 3. Plain `airflow dags test`. + scratch = g.copy_workspace(ws) + manual = g.run_dags_test(py, scratch, DAG_ID, "2026-03-10", env=g.airflow_env(scratch)) + grader.primary("airflow dags test succeeds", manual.ok, + "" if manual.ok else f"failed tasks {manual.failed_tasks()}: {manual.log[-2500:]}") + + g.standard_secondary_checks(grader, py, ws, events, import_result=imp) + grader.write(args.out) + + +def _migrated_env(py: str, ws: Path) -> dict: + env = g.airflow_env(ws) + db = g.ensure_db(py, env) + if not db.ok: + raise RuntimeError("airflow db migrate failed: " + db.tail(20)) + return env + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/calendar_yesterday/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/calendar_yesterday/dags/fx_pnl_report.py new file mode 100644 index 0000000..b83306b --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/calendar_yesterday/dags/fx_pnl_report.py @@ -0,0 +1,71 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + schedule=CronTriggerTimetable("0 6 * * 1-5", timezone=NEW_YORK), + start_date=pendulum.datetime(2026, 1, 1, tz=NEW_YORK), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(logical_date=None) -> str: + run_day = pendulum.instance(logical_date).in_timezone(NEW_YORK).date() + # the run happens the morning after the trading day it reports + return str(write_pnl_report((run_day - timedelta(days=1)).isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_interval_start/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_interval_start/dags/fx_pnl_report.py new file mode 100644 index 0000000..fb69fdc --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_interval_start/dags/fx_pnl_report.py @@ -0,0 +1,72 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + # weekdays at 06:00; the DAG timezone (from start_date) is New York + schedule="0 6 * * 1-5", + start_date=pendulum.datetime(2026, 1, 1, tz=NEW_YORK), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(data_interval_start=None) -> str: + # the data interval runs from the previous weekday's 06:00 to this run + trade_day = pendulum.instance(data_interval_start).in_timezone(NEW_YORK).date() + return str(write_pnl_report(trade_day.isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_utc_start_date/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_utc_start_date/dags/fx_pnl_report.py new file mode 100644 index 0000000..9f07b04 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/cron_string_utc_start_date/dags/fx_pnl_report.py @@ -0,0 +1,71 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + # weekdays at 06:00; the report day is computed in New York time below + schedule="0 6 * * 1-5", + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(logical_date=None) -> str: + run_day = pendulum.instance(logical_date).in_timezone(NEW_YORK).date() + return str(write_pnl_report(previous_business_day(run_day).isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/utc_eleven_am_cron/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/utc_eleven_am_cron/dags/fx_pnl_report.py new file mode 100644 index 0000000..dbe34b5 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/utc_eleven_am_cron/dags/fx_pnl_report.py @@ -0,0 +1,71 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + # 06:00 in New York is 11:00 UTC + schedule=CronTriggerTimetable("0 11 * * 1-5", timezone="UTC"), + start_date=pendulum.datetime(2026, 1, 1, tz="UTC"), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(logical_date=None) -> str: + run_day = pendulum.instance(logical_date).in_timezone(NEW_YORK).date() + return str(write_pnl_report(previous_business_day(run_day).isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/wall_clock_run_day/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/wall_clock_run_day/dags/fx_pnl_report.py new file mode 100644 index 0000000..5ed4e4b --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/mutants/wall_clock_run_day/dags/fx_pnl_report.py @@ -0,0 +1,71 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + schedule=CronTriggerTimetable("0 6 * * 1-5", timezone=NEW_YORK), + start_date=pendulum.datetime(2026, 1, 1, tz=NEW_YORK), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report() -> str: + # the run starts at 06:00 New York, so "today" there is the run day + run_day = pendulum.now(NEW_YORK).date() + return str(write_pnl_report(previous_business_day(run_day).isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/scheduling-weekday-ny-business-day/reference/dags/fx_pnl_report.py b/evals/airflow/cases/scheduling-weekday-ny-business-day/reference/dags/fx_pnl_report.py new file mode 100644 index 0000000..d2c68a3 --- /dev/null +++ b/evals/airflow/cases/scheduling-weekday-ny-business-day/reference/dags/fx_pnl_report.py @@ -0,0 +1,70 @@ +"""Daily FX P&L report for the trading desks. + +Runs at 06:00 America/New_York on weekdays and reports the previous business +day (Monday's run reports Friday). Reads the booked trades export +(data/fx_trades.csv, one row per trade with the New York trade date) and writes +reports/fx_pnl_.csv with per-desk totals. +""" + +import csv +from collections import defaultdict +from datetime import date, timedelta +from pathlib import Path + +import pendulum +from airflow.sdk import CronTriggerTimetable, dag, task + +PROJECT_DIR = Path(__file__).resolve().parents[1] +TRADES_CSV = PROJECT_DIR / "data" / "fx_trades.csv" +REPORTS_DIR = PROJECT_DIR / "reports" +NEW_YORK = "America/New_York" + + +def previous_business_day(day: date) -> date: + """The weekday before ``day`` (holidays are not considered).""" + prev = day - timedelta(days=1) + while prev.weekday() >= 5: + prev -= timedelta(days=1) + return prev + + +def write_pnl_report(trade_date: str) -> Path: + """Aggregate the trades booked on ``trade_date`` (YYYY-MM-DD) per desk.""" + totals: dict[str, list[float]] = defaultdict(lambda: [0, 0.0, 0.0]) + with TRADES_CSV.open(newline="") as fh: + for row in csv.DictReader(fh): + if row["trade_date"] != trade_date: + continue + t = totals[row["desk"]] + t[0] += 1 + t[1] += float(row["notional_usd"]) + t[2] += float(row["pnl_usd"]) + REPORTS_DIR.mkdir(parents=True, exist_ok=True) + out = REPORTS_DIR / f"fx_pnl_{trade_date}.csv" + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["desk", "trades", "notional_usd", "pnl_usd"]) + for desk in sorted(totals): + count, notional, pnl = totals[desk] + writer.writerow([desk, count, f"{notional:.2f}", f"{pnl:.2f}"]) + return out + + +@dag( + dag_id="fx_pnl_report", + schedule=CronTriggerTimetable("0 6 * * 1-5", timezone=NEW_YORK), + start_date=pendulum.datetime(2026, 1, 1, tz=NEW_YORK), + catchup=False, + default_args={"owner": "treasury-data"}, + tags=["treasury", "reports"], +) +def fx_pnl_report(): + @task + def build_report(logical_date=None) -> str: + run_day = pendulum.instance(logical_date).in_timezone(NEW_YORK).date() + return str(write_pnl_report(previous_business_day(run_day).isoformat())) + + build_report() + + +fx_pnl_report() diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/alt_reference/tests/test_shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/alt_reference/tests/test_shipments_daily.py new file mode 100644 index 0000000..bf1d1e0 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/alt_reference/tests/test_shipments_daily.py @@ -0,0 +1,76 @@ +import csv +import importlib +import os +import subprocess +import sys +from pathlib import Path + +import pytest +from airflow.models import DagBag + +REPO = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO / "dags")) +shipments = importlib.import_module("shipments_daily") + +BAG = DagBag(str(REPO / "dags"), include_examples=False) + + +def test_dagbag_loads_cleanly(): + assert not BAG.import_errors, BAG.import_errors + assert len(BAG.dags) >= 2 + + +@pytest.mark.parametrize("dag_id", ["shipments_daily", "carrier_rates_weekly"]) +def test_retry_policy(dag_id): + dag = BAG.get_dag(dag_id) + assert dag is not None + for task in dag.tasks: + assert task.retries >= 2, f"{task.task_id} has retries={task.retries}" + + +def test_schedule_is_weekdays_at_0730_utc(): + dag = BAG.get_dag("shipments_daily") + assert dag.schedule_interval == "30 7 * * 1-5" + assert str(dag.timezone.name) == "UTC" + + +def test_extract_feeds_report(): + dag = BAG.get_dag("shipments_daily") + assert dag.get_task("extract_shipments").downstream_task_ids == {"build_report"} + + +SAMPLE = [ + # id, ship, promised, delivered, expected days_late (None = not late) as of 2026-03-10 + ("late_today", "2026-03-01", "2026-03-08", "2026-03-10", 2), + ("open_overdue", "2026-03-01", "2026-03-09", "", 1), + ("open_due_today", "2026-03-01", "2026-03-10", "", None), + ("on_time_today", "2026-03-01", "2026-03-10", "2026-03-10", None), + ("late_yesterday", "2026-03-01", "2026-03-05", "2026-03-09", None), + ("future_ship", "2026-03-11", "2026-03-09", "", None), +] + + +@pytest.mark.parametrize("sid,ship,promised,delivered,expected", SAMPLE, ids=[s[0] for s in SAMPLE]) +def test_late_rules(sid, ship, promised, delivered, expected): + row = {"shipment_id": sid, "ship_date": ship, "carrier": "UPS", + "promised_date": promised, "delivered_date": delivered} + result = shipments.find_late_shipments([row], "2026-03-10") + if expected is None: + assert result == [] + else: + assert len(result) == 1 and result[0]["days_late"] == expected + + +def test_airflow_dags_test_cli(tmp_path): + report = REPO / "output" / "late_shipments" / "2026-03-03.csv" + if report.exists(): + report.unlink() + env = dict(os.environ, AIRFLOW__CORE__DAGS_FOLDER=str(REPO / "dags")) + proc = subprocess.run( + [sys.executable, "-m", "airflow", "dags", "test", "shipments_daily", "2026-03-03"], + env=env, capture_output=True, text=True, timeout=240, + ) + assert proc.returncode == 0, proc.stdout[-2000:] + proc.stderr[-2000:] + with report.open(newline="") as fh: + rows = {r["shipment_id"]: int(r["days_late"]) for r in csv.DictReader(fh)} + assert rows == {"SH-1009": 5, "SH-1003": 3, "SH-1007": 2, "SH-1001": 1} diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/case.yaml b/evals/airflow/cases/testing-airflow2-ci-suite/case.yaml new file mode 100644 index 0000000..ba34d30 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/case.yaml @@ -0,0 +1,39 @@ +id: testing-airflow2-ci-suite +area: testing +split: dev +airflow_version: "2.11" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1800 +prompt: | + We're staying on Airflow 2.11 until the platform upgrade next year, and I want a + proper pytest suite for this repo before more people start touching it. CI will + run `pytest tests/` in this venv; there's no scheduler, webserver or anything + else running there. + + Things the suite needs to catch: + - any DAG file in dags/ that doesn't import + - shipments_daily must run at 07:30 UTC Monday to Friday only + - every task in every DAG needs at least 2 retries + - in shipments_daily, the report can only be built after the export is extracted + - the late-shipment rules in shipments_daily: for a run date, a shipment is late + if it was delivered that day after its promised date, or if it still hasn't + been delivered and its promised date is before the run date. days_late counts + from the promised date to delivery (or to the run date if still open). + + Please also include one test that runs shipments_daily end to end on the sample + data in data/ for 2026-03-03. It has to pass on the current code, and please + leave the DAGs as they are. +description: | + Area: testing (Airflow 2.11). Pain point P5 (weak DAG tests) plus version + awareness in the other direction: the suite must use 2.x APIs + (airflow.models.DagBag, dag.test(execution_date=...) or the CLI), because + Airflow-3 imports (airflow.sdk, airflow.dag_processing.dagbag, the standard + provider) do not exist in 2.11. Primary checks: the suite passes on the correct + project and fails behaviourally on five hidden planted bugs: a DAG file that + imports PythonOperator from the Airflow-3 standard provider (import error in + 2.11), a Sunday-Thursday cron, undelivered shipments ignored by the late-shipment + logic, retries dropped from default_args, and build_report no longer downstream + of extract_shipments. Mutants: the reference suite written with Airflow-3 test + APIs (collection error on 2.11), and a structure/policy-only suite that never + exercises the late-shipment logic. diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/.gitignore b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/.gitignore new file mode 100644 index 0000000..7364586 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/.gitignore @@ -0,0 +1,3 @@ +output/ +__pycache__/ +.pytest_cache/ diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/README.md b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/README.md new file mode 100644 index 0000000..b1819e4 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/README.md @@ -0,0 +1,16 @@ +# logistics-pipelines + +Airflow 2.11 DAGs for the logistics ops team. We are staying on 2.11 until the +platform upgrade next year. + +- `dags/shipments_daily.py` - weekday late-shipment exceptions report + (`output/late_shipments/.csv`). +- `dags/carrier_rates_weekly.py` - weekly average freight rate per carrier. +- `data/` - sample exports used in dev. + +Local run: + +```bash +pip install -r requirements.txt +airflow dags test shipments_daily 2026-03-03 +``` diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/carrier_rates_weekly.py b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/carrier_rates_weekly.py new file mode 100644 index 0000000..d2c028d --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/carrier_rates_weekly.py @@ -0,0 +1,41 @@ +"""Weekly average freight rate per carrier, for the procurement dashboard.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "carrier_rates" + + +def summarize_rates(ds: str, **context) -> str: + totals: dict[str, list[float]] = defaultdict(list) + with (DATA_DIR / "carrier_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["carrier"]].append(float(row["rate_per_kg"])) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "avg_rate_per_kg"]) + for carrier in sorted(totals): + writer.writerow([carrier, f"{sum(totals[carrier]) / len(totals[carrier]):.3f}"]) + return str(out) + + +with DAG( + dag_id="carrier_rates_weekly", + schedule="0 9 * * 1", + start_date=datetime(2026, 1, 5), + catchup=False, + default_args={"owner": "logistics-data", "retries": 2, "retry_delay": timedelta(minutes=30)}, + tags=["logistics", "procurement"], +) as dag: + PythonOperator(task_id="summarize_rates", python_callable=summarize_rates) diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/shipments_daily.py new file mode 100644 index 0000000..c9a2848 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/dags/shipments_daily.py @@ -0,0 +1,89 @@ +"""Daily late-shipment exceptions report for the ops team. + +Runs on weekdays at 07:30 UTC. For the run date (ds) the report lists: +- shipments delivered on ds after their promised date, and +- shipments still undelivered whose promised date is before ds. + +Output: output/late_shipments/.csv +""" + +from __future__ import annotations + +import csv +from datetime import date, datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "late_shipments" +REPORT_COLUMNS = ["shipment_id", "carrier", "promised_date", "delivered_date", "days_late"] + +DEFAULT_ARGS = { + "owner": "logistics-data", + "retries": 3, + "retry_delay": timedelta(minutes=10), +} + + +def _days_between(start: str, end: str) -> int: + return (date.fromisoformat(end) - date.fromisoformat(start)).days + + +def find_late_shipments(rows: list[dict], as_of: str) -> list[dict]: + """Late shipments as of ``as_of`` (YYYY-MM-DD), worst first. + + days_late = (delivered_date, or as_of if undelivered) - promised_date, in days. + """ + late = [] + for row in rows: + if row["ship_date"] > as_of: + continue + delivered = row["delivered_date"] or "" + if delivered: + if delivered != as_of or delivered <= row["promised_date"]: + continue + days = _days_between(row["promised_date"], delivered) + else: + if row["promised_date"] >= as_of: + continue + days = _days_between(row["promised_date"], as_of) + late.append({**row, "days_late": days}) + return sorted(late, key=lambda r: (-r["days_late"], r["shipment_id"])) + + +def extract_shipments(**context) -> list[dict]: + with (DATA_DIR / "shipments.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def build_report(ds: str, ti, **context) -> str: + rows = ti.xcom_pull(task_ids="extract_shipments") or [] + late = find_late_shipments(rows, as_of=ds) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=REPORT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(late) + return str(out) + + +with DAG( + dag_id="shipments_daily", + schedule="30 7 * * 1-5", + start_date=datetime(2026, 1, 5), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["logistics"], +) as dag: + start = EmptyOperator(task_id="start") + extract = PythonOperator(task_id="extract_shipments", python_callable=extract_shipments) + report = PythonOperator(task_id="build_report", python_callable=build_report) + done = EmptyOperator(task_id="done") + + start >> extract >> report >> done diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/carrier_rates.csv b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/carrier_rates.csv new file mode 100644 index 0000000..45aa259 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/carrier_rates.csv @@ -0,0 +1,6 @@ +carrier,quote_date,lane,rate_per_kg +DHL,2026-02-23,FRA-JFK,4.120 +DHL,2026-02-23,FRA-ORD,4.380 +UPS,2026-02-23,FRA-JFK,3.950 +UPS,2026-02-23,AMS-JFK,4.010 +FedEx,2026-02-23,CDG-JFK,4.260 diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/shipments.csv b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/shipments.csv new file mode 100644 index 0000000..da8692f --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/data/shipments.csv @@ -0,0 +1,10 @@ +shipment_id,ship_date,carrier,promised_date,delivered_date +SH-1001,2026-02-26,DHL,2026-03-02,2026-03-03 +SH-1002,2026-02-27,UPS,2026-03-03,2026-03-03 +SH-1003,2026-02-25,FedEx,2026-02-28, +SH-1004,2026-03-01,DHL,2026-03-03, +SH-1005,2026-02-20,UPS,2026-02-24,2026-03-02 +SH-1006,2026-03-04,FedEx,2026-03-06, +SH-1007,2026-02-28,UPS,2026-03-01,2026-03-03 +SH-1008,2026-03-02,DHL,2026-03-04,2026-03-03 +SH-1009,2026-02-24,DHL,2026-02-26, diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/fixture/requirements.txt b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/requirements.txt new file mode 100644 index 0000000..cb9ccf0 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==2.11.2 diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/grade.py b/evals/airflow/cases/testing-airflow2-ci-suite/grade.py new file mode 100644 index 0000000..8693b5e --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/grade.py @@ -0,0 +1,175 @@ +"""Grader for testing-airflow2-ci-suite (Airflow 2.11). + +The agent writes a pytest suite. Grading runs that suite, never the agent's claims: + +1. Against the correct project (the pristine fixture sources restored over the + agent's workspace): the suite must collect at least one test and pass. +2. Against each hidden planted-bug variant (``hidden_bugs//`` overlays the + agent never sees): at least one test must fail for a behavioural reason. A + test-module collection error does not count. For planted import errors, a + test that fails at run time because the DAG file cannot be imported does count + (that is exactly what a loader test is for), and so does a collection error + (a test module that imports the DAG files at collection time). +3. Against a behaviour-preserving edit of the sources (a comment added at the top + and bottom of every ``dags/**/*.py`` plus an unused ``__review_note__`` module + attribute): the suite must still pass. This rejects snapshot suites (hashing or + diffing the DAG files or their AST) that "catch" every planted bug without + testing any behaviour. + +Each pytest run gets its own AIRFLOW_HOME with a migrated sqlite metadata DB (what a +CI job has after ``airflow db migrate``); no scheduler or other services run. +The hidden-bug and pristine-restore logic is shared by every ``testing-*`` case. +""" + +from __future__ import annotations + +import concurrent.futures as cf +import filecmp +import os +import shutil +import sys +import tempfile +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +# Fixture paths restored before running the agent's suite (the code under test). +SOURCE_PATHS = ("dags", "data") +# name -> (what the planted bug is, whether it is an import-time failure) +HIDDEN_BUGS = { + "af3_import": ("carrier_rates_weekly imports PythonOperator from the Airflow-3 standard provider (absent in 2.11)", True), + "weekend_schedule": ("shipments_daily cron changed to 30 7 * * 0-4 (Sunday-Thursday)", False), + "undelivered_ignored": ("find_late_shipments skips undelivered shipments", False), + "retries_dropped": ("shipments_daily default_args lose retries (tasks fall back to 0)", False), + "missing_dependency": ("build_report no longer runs after extract_shipments", False), +} +PYTEST_TIMEOUT = 300 +BENIGN_HEADER = "# Reviewed in PR: formatting only, behaviour unchanged.\n" +# A no-op module attribute as well as comments, so a suite that snapshots the code +# (AST or bytecode) rather than the text still counts as a snapshot suite. +BENIGN_FOOTER = '\n\n# End of module.\n__review_note__ = "formatting only, behaviour unchanged"\n' + + +def pristine_overlay(tmp: Path) -> Path: + """Overlay holding the fixture's source files (tests and configs excluded).""" + ov = tmp / "pristine" + for rel in SOURCE_PATHS: + src = CASE_DIR / "fixture" / rel + if src.is_dir(): + shutil.copytree(src, ov / rel, ignore=shutil.ignore_patterns("__pycache__")) + elif src.exists(): + (ov / rel).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, ov / rel) + return ov + + +def benign_overlay(tmp: Path) -> Path: + """Overlay with every fixture ``dags/**/*.py`` wrapped in comments plus a no-op attribute.""" + ov = tmp / "benign" + root = CASE_DIR / "fixture" + for src in (root / "dags").rglob("*.py"): + if "__pycache__" in src.parts: + continue + dst = ov / src.relative_to(root) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(BENIGN_HEADER + src.read_text().rstrip("\n") + BENIGN_FOOTER) + return ov + + +def migrated_home(py: str, tmp: Path) -> Path: + home = tmp / "af-home-template" + res = g.ensure_db(py, g.airflow_env(tmp, airflow_home=home, env_py=py)) + if not res.ok: + raise RuntimeError(f"airflow db migrate failed: {res.tail()}") + return home + + +def run_suite(py: str, ws: Path, home_template: Path) -> g.PytestResult: + home = Path(tempfile.mkdtemp(prefix="eval-af-home-")) + shutil.copytree(home_template, home, dirs_exist_ok=True) + env = g.airflow_env(ws, airflow_home=home, env_py=py) + return g.run_pytest(py, ws, ("tests",), env=env, timeout=PYTEST_TIMEOUT, + extra_args=("--continue-on-collection-errors",)) + + +def caught(res: g.PytestResult, import_kind: bool) -> bool: + if res.status == "timeout": + return False + if import_kind: + # Only the planted DAG file differs from the correct project (where the suite + # passed), so a collection error here is the suite importing that broken file: + # `pytest tests/` fails in CI, which is what the requirement asks for. + return res.status == "collection_error" or any( + t.outcome in ("failed", "error") and t.phase != "collection" for t in res.tests) + return bool(res.behavioral_failures) + + +def test_files(ws: Path) -> list[Path]: + tests = ws / "tests" + if not tests.is_dir(): + return [] + return sorted(set(tests.rglob("test_*.py")) | set(tests.rglob("*_test.py"))) + + +def changed_sources(ws: Path) -> list[str]: + changed = [] + for rel in SOURCE_PATHS: + root = CASE_DIR / "fixture" / rel + files = [p for p in root.rglob("*") if p.is_file()] if root.is_dir() else [root] + for f in files: + r = f.relative_to(CASE_DIR / "fixture") + if "__pycache__" in r.parts: + continue + if not (ws / r).is_file() or not filecmp.cmp(f, ws / r, shallow=False): + changed.append(str(r)) + return changed + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + tmp = Path(tempfile.mkdtemp(prefix="eval-testing-")) + + files = test_files(ws) + grader.primary("test suite exists under tests/", bool(files), + ", ".join(str(f.relative_to(ws)) for f in files) or "no test_*.py files under tests/") + + home = migrated_home(py, tmp) + correct = g.copy_workspace(ws, [pristine_overlay(tmp)]) + base = run_suite(py, correct, home) + base_ok = base.status == "passed" and base.passed >= 1 + grader.primary("suite passes on the correct project", base_ok, + base.summary() + ("" if base_ok else "\n" + base.output_tail[-2500:])) + + benign_ov = benign_overlay(tmp) + + def job(name: str): + overlay = benign_ov if name == "__benign__" else CASE_DIR / "hidden_bugs" / name + return name, run_suite(py, g.copy_workspace(correct, [overlay]), home) + + with cf.ThreadPoolExecutor(max_workers=3) as ex: + results = dict(ex.map(job, ["__benign__", *HIDDEN_BUGS])) + benign = results.pop("__benign__") + benign_ok = base_ok and benign.status == "passed" and benign.passed >= 1 + grader.primary("suite passes on a behaviour-preserving edit of the sources", benign_ok, + ("" if base_ok else "not attributable: the suite does not pass on the correct project; ") + + benign.summary() + ("" if benign_ok or not base_ok else "\n" + benign.output_tail[-1500:])) + for name, (what, import_kind) in HIDDEN_BUGS.items(): + res = results[name] + hit = base_ok and caught(res, import_kind) + prefix = "" if base_ok else "not attributable: the suite does not pass on the correct project; " + grader.primary(f"suite catches hidden bug: {name}", hit, f"{prefix}{what} -> {res.summary()}") + + changed = changed_sources(ws) + grader.secondary("project sources left unchanged", not changed, ", ".join(changed[:20])) + g.standard_secondary_checks(grader, py, correct, events, verify_patterns=(r"\bpytest\b",), + ruff_paths=("tests",) if files else ("dags",)) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/af3_import/dags/carrier_rates_weekly.py b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/af3_import/dags/carrier_rates_weekly.py new file mode 100644 index 0000000..d428f2b --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/af3_import/dags/carrier_rates_weekly.py @@ -0,0 +1,41 @@ +"""Weekly average freight rate per carrier, for the procurement dashboard.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.providers.standard.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "carrier_rates" + + +def summarize_rates(ds: str, **context) -> str: + totals: dict[str, list[float]] = defaultdict(list) + with (DATA_DIR / "carrier_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + totals[row["carrier"]].append(float(row["rate_per_kg"])) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["carrier", "avg_rate_per_kg"]) + for carrier in sorted(totals): + writer.writerow([carrier, f"{sum(totals[carrier]) / len(totals[carrier]):.3f}"]) + return str(out) + + +with DAG( + dag_id="carrier_rates_weekly", + schedule="0 9 * * 1", + start_date=datetime(2026, 1, 5), + catchup=False, + default_args={"owner": "logistics-data", "retries": 2, "retry_delay": timedelta(minutes=30)}, + tags=["logistics", "procurement"], +) as dag: + PythonOperator(task_id="summarize_rates", python_callable=summarize_rates) diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/missing_dependency/dags/shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/missing_dependency/dags/shipments_daily.py new file mode 100644 index 0000000..5f7eaad --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/missing_dependency/dags/shipments_daily.py @@ -0,0 +1,89 @@ +"""Daily late-shipment exceptions report for the ops team. + +Runs on weekdays at 07:30 UTC. For the run date (ds) the report lists: +- shipments delivered on ds after their promised date, and +- shipments still undelivered whose promised date is before ds. + +Output: output/late_shipments/.csv +""" + +from __future__ import annotations + +import csv +from datetime import date, datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "late_shipments" +REPORT_COLUMNS = ["shipment_id", "carrier", "promised_date", "delivered_date", "days_late"] + +DEFAULT_ARGS = { + "owner": "logistics-data", + "retries": 3, + "retry_delay": timedelta(minutes=10), +} + + +def _days_between(start: str, end: str) -> int: + return (date.fromisoformat(end) - date.fromisoformat(start)).days + + +def find_late_shipments(rows: list[dict], as_of: str) -> list[dict]: + """Late shipments as of ``as_of`` (YYYY-MM-DD), worst first. + + days_late = (delivered_date, or as_of if undelivered) - promised_date, in days. + """ + late = [] + for row in rows: + if row["ship_date"] > as_of: + continue + delivered = row["delivered_date"] or "" + if delivered: + if delivered != as_of or delivered <= row["promised_date"]: + continue + days = _days_between(row["promised_date"], delivered) + else: + if row["promised_date"] >= as_of: + continue + days = _days_between(row["promised_date"], as_of) + late.append({**row, "days_late": days}) + return sorted(late, key=lambda r: (-r["days_late"], r["shipment_id"])) + + +def extract_shipments(**context) -> list[dict]: + with (DATA_DIR / "shipments.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def build_report(ds: str, ti, **context) -> str: + rows = ti.xcom_pull(task_ids="extract_shipments") or [] + late = find_late_shipments(rows, as_of=ds) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=REPORT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(late) + return str(out) + + +with DAG( + dag_id="shipments_daily", + schedule="30 7 * * 1-5", + start_date=datetime(2026, 1, 5), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["logistics"], +) as dag: + start = EmptyOperator(task_id="start") + extract = PythonOperator(task_id="extract_shipments", python_callable=extract_shipments) + report = PythonOperator(task_id="build_report", python_callable=build_report) + done = EmptyOperator(task_id="done") + + start >> [extract, report] >> done diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/retries_dropped/dags/shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/retries_dropped/dags/shipments_daily.py new file mode 100644 index 0000000..0b31e4c --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/retries_dropped/dags/shipments_daily.py @@ -0,0 +1,87 @@ +"""Daily late-shipment exceptions report for the ops team. + +Runs on weekdays at 07:30 UTC. For the run date (ds) the report lists: +- shipments delivered on ds after their promised date, and +- shipments still undelivered whose promised date is before ds. + +Output: output/late_shipments/.csv +""" + +from __future__ import annotations + +import csv +from datetime import date, datetime +from pathlib import Path + +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "late_shipments" +REPORT_COLUMNS = ["shipment_id", "carrier", "promised_date", "delivered_date", "days_late"] + +DEFAULT_ARGS = { + "owner": "logistics-data", +} + + +def _days_between(start: str, end: str) -> int: + return (date.fromisoformat(end) - date.fromisoformat(start)).days + + +def find_late_shipments(rows: list[dict], as_of: str) -> list[dict]: + """Late shipments as of ``as_of`` (YYYY-MM-DD), worst first. + + days_late = (delivered_date, or as_of if undelivered) - promised_date, in days. + """ + late = [] + for row in rows: + if row["ship_date"] > as_of: + continue + delivered = row["delivered_date"] or "" + if delivered: + if delivered != as_of or delivered <= row["promised_date"]: + continue + days = _days_between(row["promised_date"], delivered) + else: + if row["promised_date"] >= as_of: + continue + days = _days_between(row["promised_date"], as_of) + late.append({**row, "days_late": days}) + return sorted(late, key=lambda r: (-r["days_late"], r["shipment_id"])) + + +def extract_shipments(**context) -> list[dict]: + with (DATA_DIR / "shipments.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def build_report(ds: str, ti, **context) -> str: + rows = ti.xcom_pull(task_ids="extract_shipments") or [] + late = find_late_shipments(rows, as_of=ds) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=REPORT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(late) + return str(out) + + +with DAG( + dag_id="shipments_daily", + schedule="30 7 * * 1-5", + start_date=datetime(2026, 1, 5), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["logistics"], +) as dag: + start = EmptyOperator(task_id="start") + extract = PythonOperator(task_id="extract_shipments", python_callable=extract_shipments) + report = PythonOperator(task_id="build_report", python_callable=build_report) + done = EmptyOperator(task_id="done") + + start >> extract >> report >> done diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/undelivered_ignored/dags/shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/undelivered_ignored/dags/shipments_daily.py new file mode 100644 index 0000000..2fecb61 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/undelivered_ignored/dags/shipments_daily.py @@ -0,0 +1,91 @@ +"""Daily late-shipment exceptions report for the ops team. + +Runs on weekdays at 07:30 UTC. For the run date (ds) the report lists: +- shipments delivered on ds after their promised date, and +- shipments still undelivered whose promised date is before ds. + +Output: output/late_shipments/.csv +""" + +from __future__ import annotations + +import csv +from datetime import date, datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "late_shipments" +REPORT_COLUMNS = ["shipment_id", "carrier", "promised_date", "delivered_date", "days_late"] + +DEFAULT_ARGS = { + "owner": "logistics-data", + "retries": 3, + "retry_delay": timedelta(minutes=10), +} + + +def _days_between(start: str, end: str) -> int: + return (date.fromisoformat(end) - date.fromisoformat(start)).days + + +def find_late_shipments(rows: list[dict], as_of: str) -> list[dict]: + """Late shipments as of ``as_of`` (YYYY-MM-DD), worst first. + + days_late = (delivered_date, or as_of if undelivered) - promised_date, in days. + """ + late = [] + for row in rows: + if row["ship_date"] > as_of: + continue + delivered = row["delivered_date"] or "" + if not delivered: + continue + if delivered: + if delivered != as_of or delivered <= row["promised_date"]: + continue + days = _days_between(row["promised_date"], delivered) + else: + if row["promised_date"] >= as_of: + continue + days = _days_between(row["promised_date"], as_of) + late.append({**row, "days_late": days}) + return sorted(late, key=lambda r: (-r["days_late"], r["shipment_id"])) + + +def extract_shipments(**context) -> list[dict]: + with (DATA_DIR / "shipments.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def build_report(ds: str, ti, **context) -> str: + rows = ti.xcom_pull(task_ids="extract_shipments") or [] + late = find_late_shipments(rows, as_of=ds) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=REPORT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(late) + return str(out) + + +with DAG( + dag_id="shipments_daily", + schedule="30 7 * * 1-5", + start_date=datetime(2026, 1, 5), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["logistics"], +) as dag: + start = EmptyOperator(task_id="start") + extract = PythonOperator(task_id="extract_shipments", python_callable=extract_shipments) + report = PythonOperator(task_id="build_report", python_callable=build_report) + done = EmptyOperator(task_id="done") + + start >> extract >> report >> done diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/weekend_schedule/dags/shipments_daily.py b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/weekend_schedule/dags/shipments_daily.py new file mode 100644 index 0000000..1984241 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/hidden_bugs/weekend_schedule/dags/shipments_daily.py @@ -0,0 +1,89 @@ +"""Daily late-shipment exceptions report for the ops team. + +Runs on weekdays at 07:30 UTC. For the run date (ds) the report lists: +- shipments delivered on ds after their promised date, and +- shipments still undelivered whose promised date is before ds. + +Output: output/late_shipments/.csv +""" + +from __future__ import annotations + +import csv +from datetime import date, datetime, timedelta +from pathlib import Path + +from airflow import DAG +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import PythonOperator + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "late_shipments" +REPORT_COLUMNS = ["shipment_id", "carrier", "promised_date", "delivered_date", "days_late"] + +DEFAULT_ARGS = { + "owner": "logistics-data", + "retries": 3, + "retry_delay": timedelta(minutes=10), +} + + +def _days_between(start: str, end: str) -> int: + return (date.fromisoformat(end) - date.fromisoformat(start)).days + + +def find_late_shipments(rows: list[dict], as_of: str) -> list[dict]: + """Late shipments as of ``as_of`` (YYYY-MM-DD), worst first. + + days_late = (delivered_date, or as_of if undelivered) - promised_date, in days. + """ + late = [] + for row in rows: + if row["ship_date"] > as_of: + continue + delivered = row["delivered_date"] or "" + if delivered: + if delivered != as_of or delivered <= row["promised_date"]: + continue + days = _days_between(row["promised_date"], delivered) + else: + if row["promised_date"] >= as_of: + continue + days = _days_between(row["promised_date"], as_of) + late.append({**row, "days_late": days}) + return sorted(late, key=lambda r: (-r["days_late"], r["shipment_id"])) + + +def extract_shipments(**context) -> list[dict]: + with (DATA_DIR / "shipments.csv").open(newline="") as fh: + return list(csv.DictReader(fh)) + + +def build_report(ds: str, ti, **context) -> str: + rows = ti.xcom_pull(task_ids="extract_shipments") or [] + late = find_late_shipments(rows, as_of=ds) + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=REPORT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(late) + return str(out) + + +with DAG( + dag_id="shipments_daily", + schedule="30 7 * * 0-4", + start_date=datetime(2026, 1, 5), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["logistics"], +) as dag: + start = EmptyOperator(task_id="start") + extract = PythonOperator(task_id="extract_shipments", python_callable=extract_shipments) + report = PythonOperator(task_id="build_report", python_callable=build_report) + done = EmptyOperator(task_id="done") + + start >> extract >> report >> done diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/af3_test_apis/tests/test_dags.py b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/af3_test_apis/tests/test_dags.py new file mode 100644 index 0000000..d2ae4b9 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/af3_test_apis/tests/test_dags.py @@ -0,0 +1,95 @@ +"""CI tests for the Airflow 2.11 DAGs in dags/. No scheduler needed.""" + +from __future__ import annotations + +import csv +import shutil +import sys +from pathlib import Path + +import pendulum +import pytest +from airflow.dag_processing.dagbag import DagBag +from airflow.timetables.base import TimeRestriction +from airflow.sdk import DagRunState + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DAGS_DIR = PROJECT_ROOT / "dags" +sys.path.insert(0, str(DAGS_DIR)) + +import shipments_daily # noqa: E402 + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + return DagBag(dag_folder=str(DAGS_DIR)) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, dagbag.import_errors + assert {"shipments_daily", "carrier_rates_weekly"} <= set(dagbag.dag_ids) + + +def test_every_task_retries_at_least_twice(dagbag): + low = [ + f"{dag_id}.{task.task_id}={task.retries}" + for dag_id, dag in dagbag.dags.items() + for task in dag.tasks + if (task.retries or 0) < 2 + ] + assert not low, low + + +def test_shipments_schedule_weekdays_0730_utc(dagbag): + dag = dagbag.get_dag("shipments_daily") + restriction = TimeRestriction(earliest=pendulum.datetime(2026, 3, 6, tz="UTC"), latest=None, catchup=True) + runs, last = [], None + for _ in range(10): + info = dag.timetable.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + runs.append(pendulum.instance(info.run_after).in_timezone("UTC")) + last = info.data_interval + assert all((r.hour, r.minute) == (7, 30) for r in runs), runs + assert {r.isoweekday() for r in runs} == {1, 2, 3, 4, 5}, runs + + +def test_report_depends_on_extract(dagbag): + dag = dagbag.get_dag("shipments_daily") + assert "extract_shipments" in dag.get_task("build_report").upstream_task_ids + + +def _row(sid, promised, delivered="", shipped="2026-02-20"): + return {"shipment_id": sid, "ship_date": shipped, "carrier": "DHL", + "promised_date": promised, "delivered_date": delivered} + + +def test_delivered_late_on_run_date(): + late = shipments_daily.find_late_shipments([_row("A", "2026-03-01", "2026-03-03")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("A", 2)] + + +def test_on_time_early_and_other_days_are_not_late(): + rows = [ + _row("on_time", "2026-03-03", "2026-03-03"), + _row("early", "2026-03-04", "2026-03-03"), + _row("late_other_day", "2026-02-24", "2026-03-02"), + _row("due_today_open", "2026-03-03"), + _row("not_shipped", "2026-03-01", shipped="2026-03-04"), + ] + assert shipments_daily.find_late_shipments(rows, "2026-03-03") == [] + + +def test_undelivered_past_promise_is_late(): + late = shipments_daily.find_late_shipments([_row("B", "2026-02-26")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("B", 5)] + + +def test_end_to_end_2026_03_03(dagbag): + out_dir = shipments_daily.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dag = dagbag.get_dag("shipments_daily") + dagrun = dag.test(logical_date=pendulum.datetime(2026, 3, 3, 7, 30, tz="UTC")) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-03.csv").open(newline="") as fh: + got = [(r["shipment_id"], r["days_late"]) for r in csv.DictReader(fh)] + assert got == [("SH-1009", "5"), ("SH-1003", "3"), ("SH-1007", "2"), ("SH-1001", "1")] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/_DELETE b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/_DELETE new file mode 100644 index 0000000..6af626d --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/_DELETE @@ -0,0 +1,2 @@ +# pure snapshot suite: drop the reference tests +tests/test_dags.py diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..a72f98f --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/ast_snapshot/tests/test_dag_sources.py @@ -0,0 +1,28 @@ +"""Golden-AST regression guard: fail CI when the code of any DAG file changes. + +Comments and formatting are ignored (the AST is compared), so reformatting or +documenting a DAG does not trip it; any change to the code does. +""" + +from __future__ import annotations + +import ast +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + 'carrier_rates_weekly.py': 'c08ee1337c0d8b93fa8ad91212a0004566728bedb653e34727c30733d75091b6', + 'shipments_daily.py': '4369df7c09e17570488670e0628fd4da893eec945c5ab8070e090e3ead7d9adc', +} + + +def _ast_digest(path: Path) -> str: + return hashlib.sha256(ast.dump(ast.parse(path.read_text())).encode()).hexdigest() + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_code_matches_reviewed_version(rel): + assert _ast_digest(DAGS_DIR / rel) == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/schedule_time_only/tests/test_dags.py b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/schedule_time_only/tests/test_dags.py new file mode 100644 index 0000000..6c1e486 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/schedule_time_only/tests/test_dags.py @@ -0,0 +1,94 @@ +"""CI tests for the Airflow 2.11 DAGs in dags/. No scheduler needed.""" + +from __future__ import annotations + +import csv +import shutil +import sys +from pathlib import Path + +import pendulum +import pytest +from airflow.models import DagBag +from airflow.timetables.base import TimeRestriction +from airflow.utils.state import DagRunState + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DAGS_DIR = PROJECT_ROOT / "dags" +sys.path.insert(0, str(DAGS_DIR)) + +import shipments_daily # noqa: E402 + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, dagbag.import_errors + assert {"shipments_daily", "carrier_rates_weekly"} <= set(dagbag.dag_ids) + + +def test_every_task_retries_at_least_twice(dagbag): + low = [ + f"{dag_id}.{task.task_id}={task.retries}" + for dag_id, dag in dagbag.dags.items() + for task in dag.tasks + if (task.retries or 0) < 2 + ] + assert not low, low + + +def test_shipments_schedule_0730_utc(dagbag): + dag = dagbag.get_dag("shipments_daily") + restriction = TimeRestriction(earliest=pendulum.datetime(2026, 3, 6, tz="UTC"), latest=None, catchup=True) + runs, last = [], None + for _ in range(10): + info = dag.timetable.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + runs.append(pendulum.instance(info.run_after).in_timezone("UTC")) + last = info.data_interval + assert all((r.hour, r.minute) == (7, 30) for r in runs), runs + + +def test_report_depends_on_extract(dagbag): + dag = dagbag.get_dag("shipments_daily") + assert "extract_shipments" in dag.get_task("build_report").upstream_task_ids + + +def _row(sid, promised, delivered="", shipped="2026-02-20"): + return {"shipment_id": sid, "ship_date": shipped, "carrier": "DHL", + "promised_date": promised, "delivered_date": delivered} + + +def test_delivered_late_on_run_date(): + late = shipments_daily.find_late_shipments([_row("A", "2026-03-01", "2026-03-03")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("A", 2)] + + +def test_on_time_early_and_other_days_are_not_late(): + rows = [ + _row("on_time", "2026-03-03", "2026-03-03"), + _row("early", "2026-03-04", "2026-03-03"), + _row("late_other_day", "2026-02-24", "2026-03-02"), + _row("due_today_open", "2026-03-03"), + _row("not_shipped", "2026-03-01", shipped="2026-03-04"), + ] + assert shipments_daily.find_late_shipments(rows, "2026-03-03") == [] + + +def test_undelivered_past_promise_is_late(): + late = shipments_daily.find_late_shipments([_row("B", "2026-02-26")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("B", 5)] + + +def test_end_to_end_2026_03_03(dagbag): + out_dir = shipments_daily.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dag = dagbag.get_dag("shipments_daily") + dagrun = dag.test(execution_date=pendulum.datetime(2026, 3, 3, 7, 30, tz="UTC")) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-03.csv").open(newline="") as fh: + got = [(r["shipment_id"], r["days_late"]) for r in csv.DictReader(fh)] + assert got == [("SH-1009", "5"), ("SH-1003", "3"), ("SH-1007", "2"), ("SH-1001", "1")] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/_DELETE b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/_DELETE new file mode 100644 index 0000000..6af626d --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/_DELETE @@ -0,0 +1,2 @@ +# pure snapshot suite: drop the reference tests +tests/test_dags.py diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..f6a692e --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/source_snapshot/tests/test_dag_sources.py @@ -0,0 +1,20 @@ +"""Golden-file regression guard: fail CI when any DAG source drifts from the reviewed version.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + "carrier_rates_weekly.py": "e300d508e56e89e855bd4076d7a279cfbed32524a91ca9bfcd00505acf8575e7", + "shipments_daily.py": "644e0ac4f3abd10f34cec1668d1842c80e39ef433ac481444b6babeceacde1fe", +} + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_source_matches_reviewed_version(rel): + digest = hashlib.sha256((DAGS_DIR / rel).read_bytes()).hexdigest() + assert digest == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/mutants/structure_only/tests/test_dags.py b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/structure_only/tests/test_dags.py new file mode 100644 index 0000000..d5bc453 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/mutants/structure_only/tests/test_dags.py @@ -0,0 +1,50 @@ +"""CI tests for the Airflow 2.11 DAGs in dags/. No scheduler needed.""" + +from __future__ import annotations + +from pathlib import Path + +import pendulum +import pytest +from airflow.models import DagBag +from airflow.timetables.base import TimeRestriction + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DAGS_DIR = PROJECT_ROOT / "dags" + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, dagbag.import_errors + assert {"shipments_daily", "carrier_rates_weekly"} <= set(dagbag.dag_ids) + + +def test_every_task_retries_at_least_twice(dagbag): + low = [ + f"{dag_id}.{task.task_id}={task.retries}" + for dag_id, dag in dagbag.dags.items() + for task in dag.tasks + if (task.retries or 0) < 2 + ] + assert not low, low + + +def test_shipments_schedule_weekdays_0730_utc(dagbag): + dag = dagbag.get_dag("shipments_daily") + restriction = TimeRestriction(earliest=pendulum.datetime(2026, 3, 6, tz="UTC"), latest=None, catchup=True) + runs, last = [], None + for _ in range(10): + info = dag.timetable.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + runs.append(pendulum.instance(info.run_after).in_timezone("UTC")) + last = info.data_interval + assert all((r.hour, r.minute) == (7, 30) for r in runs), runs + assert {r.isoweekday() for r in runs} == {1, 2, 3, 4, 5}, runs + + +def test_report_depends_on_extract(dagbag): + dag = dagbag.get_dag("shipments_daily") + assert "extract_shipments" in dag.get_task("build_report").upstream_task_ids diff --git a/evals/airflow/cases/testing-airflow2-ci-suite/reference/tests/test_dags.py b/evals/airflow/cases/testing-airflow2-ci-suite/reference/tests/test_dags.py new file mode 100644 index 0000000..b6a7b04 --- /dev/null +++ b/evals/airflow/cases/testing-airflow2-ci-suite/reference/tests/test_dags.py @@ -0,0 +1,95 @@ +"""CI tests for the Airflow 2.11 DAGs in dags/. No scheduler needed.""" + +from __future__ import annotations + +import csv +import shutil +import sys +from pathlib import Path + +import pendulum +import pytest +from airflow.models import DagBag +from airflow.timetables.base import TimeRestriction +from airflow.utils.state import DagRunState + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DAGS_DIR = PROJECT_ROOT / "dags" +sys.path.insert(0, str(DAGS_DIR)) + +import shipments_daily # noqa: E402 + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, dagbag.import_errors + assert {"shipments_daily", "carrier_rates_weekly"} <= set(dagbag.dag_ids) + + +def test_every_task_retries_at_least_twice(dagbag): + low = [ + f"{dag_id}.{task.task_id}={task.retries}" + for dag_id, dag in dagbag.dags.items() + for task in dag.tasks + if (task.retries or 0) < 2 + ] + assert not low, low + + +def test_shipments_schedule_weekdays_0730_utc(dagbag): + dag = dagbag.get_dag("shipments_daily") + restriction = TimeRestriction(earliest=pendulum.datetime(2026, 3, 6, tz="UTC"), latest=None, catchup=True) + runs, last = [], None + for _ in range(10): + info = dag.timetable.next_dagrun_info(last_automated_data_interval=last, restriction=restriction) + runs.append(pendulum.instance(info.run_after).in_timezone("UTC")) + last = info.data_interval + assert all((r.hour, r.minute) == (7, 30) for r in runs), runs + assert {r.isoweekday() for r in runs} == {1, 2, 3, 4, 5}, runs + + +def test_report_depends_on_extract(dagbag): + dag = dagbag.get_dag("shipments_daily") + assert "extract_shipments" in dag.get_task("build_report").upstream_task_ids + + +def _row(sid, promised, delivered="", shipped="2026-02-20"): + return {"shipment_id": sid, "ship_date": shipped, "carrier": "DHL", + "promised_date": promised, "delivered_date": delivered} + + +def test_delivered_late_on_run_date(): + late = shipments_daily.find_late_shipments([_row("A", "2026-03-01", "2026-03-03")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("A", 2)] + + +def test_on_time_early_and_other_days_are_not_late(): + rows = [ + _row("on_time", "2026-03-03", "2026-03-03"), + _row("early", "2026-03-04", "2026-03-03"), + _row("late_other_day", "2026-02-24", "2026-03-02"), + _row("due_today_open", "2026-03-03"), + _row("not_shipped", "2026-03-01", shipped="2026-03-04"), + ] + assert shipments_daily.find_late_shipments(rows, "2026-03-03") == [] + + +def test_undelivered_past_promise_is_late(): + late = shipments_daily.find_late_shipments([_row("B", "2026-02-26")], "2026-03-03") + assert [(r["shipment_id"], r["days_late"]) for r in late] == [("B", 5)] + + +def test_end_to_end_2026_03_03(dagbag): + out_dir = shipments_daily.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dag = dagbag.get_dag("shipments_daily") + dagrun = dag.test(execution_date=pendulum.datetime(2026, 3, 3, 7, 30, tz="UTC")) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-03.csv").open(newline="") as fh: + got = [(r["shipment_id"], r["days_late"]) for r in csv.DictReader(fh)] + assert got == [("SH-1009", "5"), ("SH-1003", "3"), ("SH-1007", "2"), ("SH-1001", "1")] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-custom-operator-conn/alt_reference/tests/test_csv_partition_operator.py b/evals/airflow/cases/testing-custom-operator-conn/alt_reference/tests/test_csv_partition_operator.py new file mode 100644 index 0000000..c6616fd --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/alt_reference/tests/test_csv_partition_operator.py @@ -0,0 +1,67 @@ +import os +import sys +import tempfile +import unittest +from unittest import mock + +import duckdb +from airflow.sdk import Connection + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "dags")) + +from warehouse.hooks import DuckDBWarehouseHook # noqa: E402 +from warehouse.operators import CsvPartitionToDuckDBOperator # noqa: E402 + +CSV = """refund_id,refund_date,order_id,amount +R1,2026-04-01,O1,5.00 +R2,2026-04-01,O2,6.00 +R3,2026-04-02,O3,7.00 +""" + + +class CsvPartitionToDuckDBOperatorTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.db = os.path.join(self.tmp.name, "warehouse.duckdb") + self.csv = os.path.join(self.tmp.name, "refunds.csv") + with open(self.csv, "w") as fh: + fh.write(CSV) + conns = { + "finance_wh": Connection(conn_id="finance_wh", conn_type="duckdb", host=self.db, schema="finance"), + } + patcher = mock.patch.object(DuckDBWarehouseHook, "get_connection", side_effect=lambda cid: conns[cid]) + self.get_connection = patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(self.tmp.cleanup) + + def run_for(self, day): + op = CsvPartitionToDuckDBOperator( + task_id="load_refunds", + csv_path=self.csv, + table="refunds", + partition_column="refund_date", + partition_date=day, + duckdb_conn_id="finance_wh", + ) + return op.execute(context={}) + + def table(self): + with duckdb.connect(self.db, read_only=True) as con: + return con.execute("SELECT refund_id, refund_date FROM finance.refunds ORDER BY refund_id").fetchall() + + def test_first_load(self): + self.assertEqual(self.run_for("2026-04-01"), 2) + self.assertEqual(self.table(), [("R1", "2026-04-01"), ("R2", "2026-04-01")]) + self.get_connection.assert_called_with("finance_wh") + + def test_idempotent_rerun(self): + self.run_for("2026-04-01") + self.run_for("2026-04-02") + self.run_for("2026-04-01") + self.assertEqual( + self.table(), [("R1", "2026-04-01"), ("R2", "2026-04-01"), ("R3", "2026-04-02")] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/evals/airflow/cases/testing-custom-operator-conn/case.yaml b/evals/airflow/cases/testing-custom-operator-conn/case.yaml new file mode 100644 index 0000000..63eb192 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/case.yaml @@ -0,0 +1,36 @@ +id: testing-custom-operator-conn +area: testing +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1800 +prompt: | + I wrote a custom hook and operator in dags/warehouse/ (DuckDBWarehouseHook and + CsvPartitionToDuckDBOperator) that our load DAGs use, and they have no tests at + all. Please add unit tests under tests/ that CI can run with plain `pytest tests/`. + CI has no Airflow connections configured and nothing else running, so the tests + have to supply the connection themselves. I want execute() exercised for real + against a temporary DuckDB file, not DuckDB mocked away. + + The contract the DAGs rely on: + - the hook takes the DuckDB file path from the connection's host and the target + schema from the connection's schema field, and the operator uses whatever + duckdb_conn_id it is given + - only the rows for partition_date get loaded, and execute() returns how many + rows it loaded + - re-running the same partition replaces that partition's rows (no duplicates) + and leaves other dates alone + + Please don't change the hook or operator code. +description: | + Area: testing (Airflow 3.3). Unit-testing a custom operator and hook with a mocked + Airflow connection (AIRFLOW_CONN_* env var or a patched get_connection) and no + metadata DB, calling execute() directly. Primary checks: the suite passes on the + correct code and fails behaviourally on five hidden planted bugs: hook reads the + file path from extras (falls back to :memory:, so only a test that inspects the + file catches it), schema taken from extras, operator ignoring duckdb_conn_id, a + missing DELETE (duplicates on re-run) and a dropped partition filter (return value + still correct). Mutants: tests that depend on a connection created with + `airflow connections add` (fails in clean CI), tests that mock DuckDB entirely, + and a return-value-only smoke test. diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/.gitignore b/evals/airflow/cases/testing-custom-operator-conn/fixture/.gitignore new file mode 100644 index 0000000..765e99e --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/.gitignore @@ -0,0 +1,3 @@ +*.duckdb +__pycache__/ +.pytest_cache/ diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/README.md b/evals/airflow/cases/testing-custom-operator-conn/fixture/README.md new file mode 100644 index 0000000..bac7141 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/README.md @@ -0,0 +1,14 @@ +# sales-warehouse + +Airflow 3.3 project that loads the nightly sales exports into a DuckDB warehouse. + +- `dags/load_raw_sales.py` - loads `data/orders.csv` and `data/refunds.csv` one day at a time. +- `dags/warehouse/` - our DuckDB hook and the CSV partition loader operator + (listed in `dags/.airflowignore` so the DAG processor does not parse it as DAGs). + +The DAG uses the `warehouse` connection. For a local run: + +```bash +export AIRFLOW_CONN_WAREHOUSE='{"conn_type": "duckdb", "host": "/tmp/warehouse.duckdb", "schema": "raw"}' +airflow dags test load_raw_sales 2026-03-02 +``` diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/.airflowignore b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/.airflowignore new file mode 100644 index 0000000..e339eee --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/.airflowignore @@ -0,0 +1 @@ +warehouse/ diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/load_raw_sales.py b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/load_raw_sales.py new file mode 100644 index 0000000..ccdfa47 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/load_raw_sales.py @@ -0,0 +1,38 @@ +"""Load the nightly sales exports into the raw layer of the warehouse.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import DAG +from warehouse.operators import CsvPartitionToDuckDBOperator + +DATA_DIR = Path(__file__).resolve().parents[1] / "data" + +with DAG( + dag_id="load_raw_sales", + schedule="0 3 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + default_args={"owner": "data-platform", "retries": 2, "retry_delay": timedelta(minutes=5)}, + tags=["warehouse", "raw"], +): + load_orders = CsvPartitionToDuckDBOperator( + task_id="load_orders", + csv_path=str(DATA_DIR / "orders.csv"), + table="orders", + partition_column="order_date", + partition_date="{{ ds }}", + duckdb_conn_id="warehouse", + ) + load_refunds = CsvPartitionToDuckDBOperator( + task_id="load_refunds", + csv_path=str(DATA_DIR / "refunds.csv"), + table="refunds", + partition_column="refund_date", + partition_date="{{ ds }}", + duckdb_conn_id="warehouse", + ) + + load_orders >> load_refunds diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/__init__.py b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/__init__.py new file mode 100644 index 0000000..adb48b6 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/__init__.py @@ -0,0 +1 @@ +"""Warehouse hooks and operators shared by the load DAGs.""" diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/hooks.py b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/hooks.py new file mode 100644 index 0000000..6b0b84f --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/hooks.py @@ -0,0 +1,52 @@ +"""Hook for the DuckDB warehouse file.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseHook + + +class DuckDBWarehouseHook(BaseHook): + """Opens the DuckDB database configured on an Airflow connection. + + Connection fields: + host - path to the .duckdb file (required) + schema - schema that tables are written to (default ``main``) + extra - ``{"read_only": true}`` opens the file read-only + """ + + conn_name_attr = "duckdb_conn_id" + default_conn_name = "duckdb_warehouse" + conn_type = "duckdb" + hook_name = "DuckDB warehouse" + + def __init__(self, duckdb_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.duckdb_conn_id = duckdb_conn_id + self._connection = None + + @property + def connection(self): + if self._connection is None: + self._connection = self.get_connection(self.duckdb_conn_id) + return self._connection + + @property + def database_path(self) -> str: + path = self.connection.host + if not path: + raise ValueError( + f"Connection {self.duckdb_conn_id!r} has no host; set it to the DuckDB file path" + ) + return path + + @property + def target_schema(self) -> str: + return self.connection.schema or "main" + + def get_conn(self): + import duckdb + + read_only = bool(self.connection.extra_dejson.get("read_only", False)) + return duckdb.connect(self.database_path, read_only=read_only) diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/operators.py b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/operators.py new file mode 100644 index 0000000..20e31be --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/dags/warehouse/operators.py @@ -0,0 +1,63 @@ +"""Operators that load files into the DuckDB warehouse.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseOperator +from warehouse.hooks import DuckDBWarehouseHook + + +class CsvPartitionToDuckDBOperator(BaseOperator): + """Load one date partition of a CSV export into a DuckDB table. + + The rows of ``csv_path`` whose ``partition_column`` equals ``partition_date`` + replace that partition in ``.
``, so re-runs and + backfills are idempotent. All columns are loaded as text. Returns the number of + rows loaded. + """ + + template_fields = ("csv_path", "partition_date") + ui_color = "#fff3b0" + + def __init__( + self, + *, + csv_path: str, + table: str, + partition_column: str, + partition_date: str, + duckdb_conn_id: str = DuckDBWarehouseHook.default_conn_name, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.csv_path = csv_path + self.table = table + self.partition_column = partition_column + self.partition_date = partition_date + self.duckdb_conn_id = duckdb_conn_id + + def execute(self, context) -> int: + hook = DuckDBWarehouseHook(duckdb_conn_id=self.duckdb_conn_id) + schema = hook.target_schema + target = f'"{schema}"."{self.table}"' + column = f'"{self.partition_column}"' + source = "read_csv_auto(?, all_varchar = true)" + con = hook.get_conn() + try: + con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"') + con.execute(f"CREATE TABLE IF NOT EXISTS {target} AS SELECT * FROM {source} LIMIT 0", [self.csv_path]) + con.execute("BEGIN TRANSACTION") + con.execute(f"DELETE FROM {target} WHERE {column} = ?", [self.partition_date]) + con.execute( + f"INSERT INTO {target} SELECT * FROM {source} WHERE {column} = ?", + [self.csv_path, self.partition_date], + ) + loaded = con.execute( + f"SELECT count(*) FROM {target} WHERE {column} = ?", [self.partition_date] + ).fetchone()[0] + con.execute("COMMIT") + finally: + con.close() + self.log.info("Loaded %s rows into %s for %s", loaded, target, self.partition_date) + return loaded diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/data/orders.csv b/evals/airflow/cases/testing-custom-operator-conn/fixture/data/orders.csv new file mode 100644 index 0000000..3e48446 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/data/orders.csv @@ -0,0 +1,7 @@ +order_id,order_date,customer_id,amount +O-2001,2026-03-01,C-11,54.00 +O-2002,2026-03-01,C-12,12.75 +O-2003,2026-03-02,C-11,80.10 +O-2004,2026-03-02,C-13,19.99 +O-2005,2026-03-02,C-14,240.00 +O-2006,2026-03-03,C-12,33.30 diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/data/refunds.csv b/evals/airflow/cases/testing-custom-operator-conn/fixture/data/refunds.csv new file mode 100644 index 0000000..3797ba3 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/data/refunds.csv @@ -0,0 +1,3 @@ +refund_id,refund_date,order_id,amount +R-501,2026-03-02,O-2001,54.00 +R-502,2026-03-03,O-2004,19.99 diff --git a/evals/airflow/cases/testing-custom-operator-conn/fixture/requirements.txt b/evals/airflow/cases/testing-custom-operator-conn/fixture/requirements.txt new file mode 100644 index 0000000..b76fe32 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +duckdb>=1.1 diff --git a/evals/airflow/cases/testing-custom-operator-conn/grade.py b/evals/airflow/cases/testing-custom-operator-conn/grade.py new file mode 100644 index 0000000..e01ad2b --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/grade.py @@ -0,0 +1,170 @@ +"""Grader for testing-custom-operator-conn (Airflow 3.3). + +The agent writes a pytest suite. Grading runs that suite, never the agent's claims: + +1. Against the correct project (the pristine fixture sources restored over the + agent's workspace): the suite must collect at least one test and pass. +2. Against each hidden planted-bug variant (``hidden_bugs//`` overlays the + agent never sees): at least one test must fail for a behavioural reason. A + test-module collection error does not count. For planted import errors, a + test that fails at run time because the DAG file cannot be imported does count + (that is exactly what a loader test is for). +3. Against a behaviour-preserving edit of the sources (a comment added at the top + and bottom of every ``dags/**/*.py`` plus an unused ``__review_note__`` module + attribute): the suite must still pass. This rejects snapshot suites (hashing or + diffing the DAG files or their AST) that "catch" every planted bug without + testing any behaviour. + +Each pytest run gets its own AIRFLOW_HOME with a migrated sqlite metadata DB (what a +CI job has after ``airflow db migrate``); no scheduler or other services run. +The hidden-bug and pristine-restore logic is shared by every ``testing-*`` case. +""" + +from __future__ import annotations + +import concurrent.futures as cf +import filecmp +import os +import shutil +import sys +import tempfile +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +# Fixture paths restored before running the agent's suite (the code under test). +SOURCE_PATHS = ("dags", "data") +# name -> (what the planted bug is, whether it is an import-time failure) +HIDDEN_BUGS = { + "hook_ignores_host": ("hook reads the DuckDB path from extra['path'] (default :memory:), not conn.host", False), + "schema_from_extra": ("hook reads the target schema from extra['schema'] instead of conn.schema", False), + "conn_id_ignored": ("operator builds the hook with the default conn id, ignoring duckdb_conn_id", False), + "rerun_duplicates": ("operator no longer deletes the partition before inserting (re-runs duplicate)", False), + "partition_filter_dropped": ("operator inserts every row of the CSV, not just partition_date", False), +} +PYTEST_TIMEOUT = 300 +BENIGN_HEADER = "# Reviewed in PR: formatting only, behaviour unchanged.\n" +# A no-op module attribute as well as comments, so a suite that snapshots the code +# (AST or bytecode) rather than the text still counts as a snapshot suite. +BENIGN_FOOTER = '\n\n# End of module.\n__review_note__ = "formatting only, behaviour unchanged"\n' + + +def pristine_overlay(tmp: Path) -> Path: + """Overlay holding the fixture's source files (tests and configs excluded).""" + ov = tmp / "pristine" + for rel in SOURCE_PATHS: + src = CASE_DIR / "fixture" / rel + if src.is_dir(): + shutil.copytree(src, ov / rel, ignore=shutil.ignore_patterns("__pycache__")) + elif src.exists(): + (ov / rel).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, ov / rel) + return ov + + +def benign_overlay(tmp: Path) -> Path: + """Overlay with every fixture ``dags/**/*.py`` wrapped in comments plus a no-op attribute.""" + ov = tmp / "benign" + root = CASE_DIR / "fixture" + for src in (root / "dags").rglob("*.py"): + if "__pycache__" in src.parts: + continue + dst = ov / src.relative_to(root) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(BENIGN_HEADER + src.read_text().rstrip("\n") + BENIGN_FOOTER) + return ov + + +def migrated_home(py: str, tmp: Path) -> Path: + home = tmp / "af-home-template" + res = g.ensure_db(py, g.airflow_env(tmp, airflow_home=home, env_py=py)) + if not res.ok: + raise RuntimeError(f"airflow db migrate failed: {res.tail()}") + return home + + +def run_suite(py: str, ws: Path, home_template: Path) -> g.PytestResult: + home = Path(tempfile.mkdtemp(prefix="eval-af-home-")) + shutil.copytree(home_template, home, dirs_exist_ok=True) + env = g.airflow_env(ws, airflow_home=home, env_py=py) + return g.run_pytest(py, ws, ("tests",), env=env, timeout=PYTEST_TIMEOUT, + extra_args=("--continue-on-collection-errors",)) + + +def caught(res: g.PytestResult, import_kind: bool) -> bool: + if res.status == "timeout": + return False + if import_kind: + return any(t.outcome in ("failed", "error") and t.phase != "collection" for t in res.tests) + return bool(res.behavioral_failures) + + +def test_files(ws: Path) -> list[Path]: + tests = ws / "tests" + if not tests.is_dir(): + return [] + return sorted(set(tests.rglob("test_*.py")) | set(tests.rglob("*_test.py"))) + + +def changed_sources(ws: Path) -> list[str]: + changed = [] + for rel in SOURCE_PATHS: + root = CASE_DIR / "fixture" / rel + files = [p for p in root.rglob("*") if p.is_file()] if root.is_dir() else [root] + for f in files: + r = f.relative_to(CASE_DIR / "fixture") + if "__pycache__" in r.parts: + continue + if not (ws / r).is_file() or not filecmp.cmp(f, ws / r, shallow=False): + changed.append(str(r)) + return changed + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + tmp = Path(tempfile.mkdtemp(prefix="eval-testing-")) + + files = test_files(ws) + grader.primary("test suite exists under tests/", bool(files), + ", ".join(str(f.relative_to(ws)) for f in files) or "no test_*.py files under tests/") + + home = migrated_home(py, tmp) + correct = g.copy_workspace(ws, [pristine_overlay(tmp)]) + base = run_suite(py, correct, home) + base_ok = base.status == "passed" and base.passed >= 1 + grader.primary("suite passes on the correct project", base_ok, + base.summary() + ("" if base_ok else "\n" + base.output_tail[-2500:])) + + benign_ov = benign_overlay(tmp) + + def job(name: str): + overlay = benign_ov if name == "__benign__" else CASE_DIR / "hidden_bugs" / name + return name, run_suite(py, g.copy_workspace(correct, [overlay]), home) + + with cf.ThreadPoolExecutor(max_workers=3) as ex: + results = dict(ex.map(job, ["__benign__", *HIDDEN_BUGS])) + benign = results.pop("__benign__") + benign_ok = base_ok and benign.status == "passed" and benign.passed >= 1 + grader.primary("suite passes on a behaviour-preserving edit of the sources", benign_ok, + ("" if base_ok else "not attributable: the suite does not pass on the correct project; ") + + benign.summary() + ("" if benign_ok or not base_ok else "\n" + benign.output_tail[-1500:])) + for name, (what, import_kind) in HIDDEN_BUGS.items(): + res = results[name] + hit = base_ok and caught(res, import_kind) + prefix = "" if base_ok else "not attributable: the suite does not pass on the correct project; " + grader.primary(f"suite catches hidden bug: {name}", hit, f"{prefix}{what} -> {res.summary()}") + + changed = changed_sources(ws) + grader.secondary("project sources left unchanged", not changed, ", ".join(changed[:20])) + g.standard_secondary_checks(grader, py, correct, events, verify_patterns=(r"\bpytest\b",), + ruff_paths=("tests",) if files else ("dags",)) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/conn_id_ignored/dags/warehouse/operators.py b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/conn_id_ignored/dags/warehouse/operators.py new file mode 100644 index 0000000..811f1bc --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/conn_id_ignored/dags/warehouse/operators.py @@ -0,0 +1,63 @@ +"""Operators that load files into the DuckDB warehouse.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseOperator +from warehouse.hooks import DuckDBWarehouseHook + + +class CsvPartitionToDuckDBOperator(BaseOperator): + """Load one date partition of a CSV export into a DuckDB table. + + The rows of ``csv_path`` whose ``partition_column`` equals ``partition_date`` + replace that partition in ``.
``, so re-runs and + backfills are idempotent. All columns are loaded as text. Returns the number of + rows loaded. + """ + + template_fields = ("csv_path", "partition_date") + ui_color = "#fff3b0" + + def __init__( + self, + *, + csv_path: str, + table: str, + partition_column: str, + partition_date: str, + duckdb_conn_id: str = DuckDBWarehouseHook.default_conn_name, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.csv_path = csv_path + self.table = table + self.partition_column = partition_column + self.partition_date = partition_date + self.duckdb_conn_id = duckdb_conn_id + + def execute(self, context) -> int: + hook = DuckDBWarehouseHook() + schema = hook.target_schema + target = f'"{schema}"."{self.table}"' + column = f'"{self.partition_column}"' + source = "read_csv_auto(?, all_varchar = true)" + con = hook.get_conn() + try: + con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"') + con.execute(f"CREATE TABLE IF NOT EXISTS {target} AS SELECT * FROM {source} LIMIT 0", [self.csv_path]) + con.execute("BEGIN TRANSACTION") + con.execute(f"DELETE FROM {target} WHERE {column} = ?", [self.partition_date]) + con.execute( + f"INSERT INTO {target} SELECT * FROM {source} WHERE {column} = ?", + [self.csv_path, self.partition_date], + ) + loaded = con.execute( + f"SELECT count(*) FROM {target} WHERE {column} = ?", [self.partition_date] + ).fetchone()[0] + con.execute("COMMIT") + finally: + con.close() + self.log.info("Loaded %s rows into %s for %s", loaded, target, self.partition_date) + return loaded diff --git a/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/hook_ignores_host/dags/warehouse/hooks.py b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/hook_ignores_host/dags/warehouse/hooks.py new file mode 100644 index 0000000..2ac101e --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/hook_ignores_host/dags/warehouse/hooks.py @@ -0,0 +1,48 @@ +"""Hook for the DuckDB warehouse file.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseHook + + +class DuckDBWarehouseHook(BaseHook): + """Opens the DuckDB database configured on an Airflow connection. + + Connection fields: + host - path to the .duckdb file (required) + schema - schema that tables are written to (default ``main``) + extra - ``{"read_only": true}`` opens the file read-only + """ + + conn_name_attr = "duckdb_conn_id" + default_conn_name = "duckdb_warehouse" + conn_type = "duckdb" + hook_name = "DuckDB warehouse" + + def __init__(self, duckdb_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.duckdb_conn_id = duckdb_conn_id + self._connection = None + + @property + def connection(self): + if self._connection is None: + self._connection = self.get_connection(self.duckdb_conn_id) + return self._connection + + @property + def database_path(self) -> str: + # Newer connections keep the file path in extras. + return self.connection.extra_dejson.get("path", ":memory:") + + @property + def target_schema(self) -> str: + return self.connection.schema or "main" + + def get_conn(self): + import duckdb + + read_only = bool(self.connection.extra_dejson.get("read_only", False)) + return duckdb.connect(self.database_path, read_only=read_only) diff --git a/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/partition_filter_dropped/dags/warehouse/operators.py b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/partition_filter_dropped/dags/warehouse/operators.py new file mode 100644 index 0000000..6797643 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/partition_filter_dropped/dags/warehouse/operators.py @@ -0,0 +1,63 @@ +"""Operators that load files into the DuckDB warehouse.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseOperator +from warehouse.hooks import DuckDBWarehouseHook + + +class CsvPartitionToDuckDBOperator(BaseOperator): + """Load one date partition of a CSV export into a DuckDB table. + + The rows of ``csv_path`` whose ``partition_column`` equals ``partition_date`` + replace that partition in ``.
``, so re-runs and + backfills are idempotent. All columns are loaded as text. Returns the number of + rows loaded. + """ + + template_fields = ("csv_path", "partition_date") + ui_color = "#fff3b0" + + def __init__( + self, + *, + csv_path: str, + table: str, + partition_column: str, + partition_date: str, + duckdb_conn_id: str = DuckDBWarehouseHook.default_conn_name, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.csv_path = csv_path + self.table = table + self.partition_column = partition_column + self.partition_date = partition_date + self.duckdb_conn_id = duckdb_conn_id + + def execute(self, context) -> int: + hook = DuckDBWarehouseHook(duckdb_conn_id=self.duckdb_conn_id) + schema = hook.target_schema + target = f'"{schema}"."{self.table}"' + column = f'"{self.partition_column}"' + source = "read_csv_auto(?, all_varchar = true)" + con = hook.get_conn() + try: + con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"') + con.execute(f"CREATE TABLE IF NOT EXISTS {target} AS SELECT * FROM {source} LIMIT 0", [self.csv_path]) + con.execute("BEGIN TRANSACTION") + con.execute(f"DELETE FROM {target} WHERE {column} = ?", [self.partition_date]) + con.execute( + f"INSERT INTO {target} SELECT * FROM {source}", + [self.csv_path], + ) + loaded = con.execute( + f"SELECT count(*) FROM {target} WHERE {column} = ?", [self.partition_date] + ).fetchone()[0] + con.execute("COMMIT") + finally: + con.close() + self.log.info("Loaded %s rows into %s for %s", loaded, target, self.partition_date) + return loaded diff --git a/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/rerun_duplicates/dags/warehouse/operators.py b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/rerun_duplicates/dags/warehouse/operators.py new file mode 100644 index 0000000..443c56a --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/rerun_duplicates/dags/warehouse/operators.py @@ -0,0 +1,62 @@ +"""Operators that load files into the DuckDB warehouse.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseOperator +from warehouse.hooks import DuckDBWarehouseHook + + +class CsvPartitionToDuckDBOperator(BaseOperator): + """Load one date partition of a CSV export into a DuckDB table. + + The rows of ``csv_path`` whose ``partition_column`` equals ``partition_date`` + replace that partition in ``.
``, so re-runs and + backfills are idempotent. All columns are loaded as text. Returns the number of + rows loaded. + """ + + template_fields = ("csv_path", "partition_date") + ui_color = "#fff3b0" + + def __init__( + self, + *, + csv_path: str, + table: str, + partition_column: str, + partition_date: str, + duckdb_conn_id: str = DuckDBWarehouseHook.default_conn_name, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.csv_path = csv_path + self.table = table + self.partition_column = partition_column + self.partition_date = partition_date + self.duckdb_conn_id = duckdb_conn_id + + def execute(self, context) -> int: + hook = DuckDBWarehouseHook(duckdb_conn_id=self.duckdb_conn_id) + schema = hook.target_schema + target = f'"{schema}"."{self.table}"' + column = f'"{self.partition_column}"' + source = "read_csv_auto(?, all_varchar = true)" + con = hook.get_conn() + try: + con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"') + con.execute(f"CREATE TABLE IF NOT EXISTS {target} AS SELECT * FROM {source} LIMIT 0", [self.csv_path]) + con.execute("BEGIN TRANSACTION") + con.execute( + f"INSERT INTO {target} SELECT * FROM {source} WHERE {column} = ?", + [self.csv_path, self.partition_date], + ) + loaded = con.execute( + f"SELECT count(*) FROM {target} WHERE {column} = ?", [self.partition_date] + ).fetchone()[0] + con.execute("COMMIT") + finally: + con.close() + self.log.info("Loaded %s rows into %s for %s", loaded, target, self.partition_date) + return loaded diff --git a/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/schema_from_extra/dags/warehouse/hooks.py b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/schema_from_extra/dags/warehouse/hooks.py new file mode 100644 index 0000000..13fcae1 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/hidden_bugs/schema_from_extra/dags/warehouse/hooks.py @@ -0,0 +1,52 @@ +"""Hook for the DuckDB warehouse file.""" + +from __future__ import annotations + +from typing import Any + +from airflow.sdk import BaseHook + + +class DuckDBWarehouseHook(BaseHook): + """Opens the DuckDB database configured on an Airflow connection. + + Connection fields: + host - path to the .duckdb file (required) + schema - schema that tables are written to (default ``main``) + extra - ``{"read_only": true}`` opens the file read-only + """ + + conn_name_attr = "duckdb_conn_id" + default_conn_name = "duckdb_warehouse" + conn_type = "duckdb" + hook_name = "DuckDB warehouse" + + def __init__(self, duckdb_conn_id: str = default_conn_name, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.duckdb_conn_id = duckdb_conn_id + self._connection = None + + @property + def connection(self): + if self._connection is None: + self._connection = self.get_connection(self.duckdb_conn_id) + return self._connection + + @property + def database_path(self) -> str: + path = self.connection.host + if not path: + raise ValueError( + f"Connection {self.duckdb_conn_id!r} has no host; set it to the DuckDB file path" + ) + return path + + @property + def target_schema(self) -> str: + return self.connection.extra_dejson.get("schema", "main") + + def get_conn(self): + import duckdb + + read_only = bool(self.connection.extra_dejson.get("read_only", False)) + return duckdb.connect(self.database_path, read_only=read_only) diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/_DELETE b/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/_DELETE new file mode 100644 index 0000000..fe5a617 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/_DELETE @@ -0,0 +1,3 @@ +# pure snapshot suite: drop the reference tests +tests/test_warehouse.py +tests/conftest.py diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..838e7ff --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/ast_snapshot/tests/test_dag_sources.py @@ -0,0 +1,30 @@ +"""Golden-AST regression guard: fail CI when the code of any DAG file changes. + +Comments and formatting are ignored (the AST is compared), so reformatting or +documenting a DAG does not trip it; any change to the code does. +""" + +from __future__ import annotations + +import ast +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + 'load_raw_sales.py': '1258cfcf81705234fca893c1d5d1ce11382c68778f4d719b66751b0c455afbae', + 'warehouse/__init__.py': '3d8c9237b838785019b7ebfbc7f3a5694c298cbcae817d6061dd37bc53857a00', + 'warehouse/hooks.py': 'fc003b88ff0b2af524864d7ea0b44fe367570197c63484897397bc74740a22dd', + 'warehouse/operators.py': '5752d2c03447620e7a94f9dfc2b26feb7837e93d126e34724bad2f5d0715f65b', +} + + +def _ast_digest(path: Path) -> str: + return hashlib.sha256(ast.dump(ast.parse(path.read_text())).encode()).hexdigest() + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_code_matches_reviewed_version(rel): + assert _ast_digest(DAGS_DIR / rel) == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/conn_from_metadata_db/tests/test_warehouse.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/conn_from_metadata_db/tests/test_warehouse.py new file mode 100644 index 0000000..90c0579 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/conn_from_metadata_db/tests/test_warehouse.py @@ -0,0 +1,32 @@ +"""Tests that rely on the `warehouse` connection created locally with +`airflow connections add warehouse --conn-json ...` (not present in CI).""" + +from __future__ import annotations + +import duckdb +from warehouse.hooks import DuckDBWarehouseHook +from warehouse.operators import CsvPartitionToDuckDBOperator + + +def make_op(csv_path, day): + return CsvPartitionToDuckDBOperator( + task_id="load", + csv_path=str(csv_path), + table="orders_test", + partition_column="order_date", + partition_date=day, + duckdb_conn_id="warehouse", + ) + + +def test_loads_partition_and_is_idempotent(tmp_path): + csv_path = tmp_path / "orders.csv" + csv_path.write_text("order_id,order_date\nA1,2026-03-01\nA2,2026-03-02\nA3,2026-03-02\n") + hook = DuckDBWarehouseHook(duckdb_conn_id="warehouse") + make_op(csv_path, "2026-03-01").execute(context={}) + make_op(csv_path, "2026-03-02").execute(context={}) + assert make_op(csv_path, "2026-03-02").execute(context={}) == 2 + con = duckdb.connect(hook.database_path, read_only=True) + got = con.execute(f"select order_id from {hook.target_schema}.orders_test order by 1").fetchall() + con.close() + assert got == [("A1",), ("A2",), ("A3",)] diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/mocked_duckdb/tests/test_warehouse.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/mocked_duckdb/tests/test_warehouse.py new file mode 100644 index 0000000..b50ccc0 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/mocked_duckdb/tests/test_warehouse.py @@ -0,0 +1,29 @@ +"""Operator tests with DuckDB and the connection fully mocked.""" + +from __future__ import annotations + +from unittest import mock + +from warehouse.hooks import DuckDBWarehouseHook +from warehouse.operators import CsvPartitionToDuckDBOperator + + +def test_execute_deletes_then_inserts_partition(): + fake_conn = mock.MagicMock(host="/tmp/wh.duckdb", schema="raw", extra_dejson={}) + with mock.patch.object(DuckDBWarehouseHook, "get_connection", return_value=fake_conn), \ + mock.patch("duckdb.connect") as connect: + con = connect.return_value + con.execute.return_value.fetchone.return_value = (2,) + op = CsvPartitionToDuckDBOperator( + task_id="load", + csv_path="orders.csv", + table="orders", + partition_column="order_date", + partition_date="2026-03-02", + duckdb_conn_id="warehouse", + ) + assert op.execute(context={}) == 2 + statements = " ".join(str(c.args[0]) for c in con.execute.call_args_list) + assert "DELETE" in statements + assert "INSERT" in statements + con.close.assert_called_once() diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/conftest.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/conftest.py new file mode 100644 index 0000000..8b4f28d --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + +# Airflow puts the DAGs folder on sys.path when it parses DAGs; do the same for tests. +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/test_warehouse.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/test_warehouse.py new file mode 100644 index 0000000..941fc53 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/patched_conn_any_id/tests/test_warehouse.py @@ -0,0 +1,79 @@ +"""Unit tests for the DuckDB warehouse hook and CSV partition loader (get_connection patched).""" + +from __future__ import annotations + +from unittest import mock + +import duckdb +import pytest +from airflow.sdk import Connection +from warehouse.hooks import DuckDBWarehouseHook +from warehouse.operators import CsvPartitionToDuckDBOperator + +CONN_ID = "test_wh" + + +@pytest.fixture +def db_path(tmp_path, monkeypatch): + path = tmp_path / "wh.duckdb" + conn = Connection(conn_id=CONN_ID, conn_type="duckdb", host=str(path), schema="raw_test") + # Every lookup returns the test connection, whatever conn id is asked for. + patcher = mock.patch.object(DuckDBWarehouseHook, "get_connection", return_value=conn) + patcher.start() + yield path + patcher.stop() + + +@pytest.fixture +def csv_path(tmp_path): + path = tmp_path / "orders.csv" + path.write_text( + "order_id,order_date,amount\n" + "A1,2026-03-01,10.00\n" + "A2,2026-03-02,20.00\n" + "A3,2026-03-02,30.00\n" + "A4,2026-03-03,40.00\n" + ) + return path + + +def make_op(csv_path, day, conn_id=CONN_ID): + return CsvPartitionToDuckDBOperator( + task_id="load", + csv_path=str(csv_path), + table="orders", + partition_column="order_date", + partition_date=day, + duckdb_conn_id=conn_id, + ) + + +def rows(db_path, sql): + con = duckdb.connect(str(db_path), read_only=True) + try: + return con.execute(sql).fetchall() + finally: + con.close() + + +def test_hook_reads_connection_fields(db_path): + hook = DuckDBWarehouseHook(duckdb_conn_id=CONN_ID) + assert hook.database_path == str(db_path) + assert hook.target_schema == "raw_test" + con = hook.get_conn() + con.execute("create table t as select 1 as x") + con.close() + assert rows(db_path, "select x from t") == [(1,)] + + +def test_loads_only_the_partition_into_connection_schema(db_path, csv_path): + loaded = make_op(csv_path, "2026-03-02").execute(context={}) + assert loaded == 2 + assert rows(db_path, "select order_id from raw_test.orders order by 1") == [("A2",), ("A3",)] + + +def test_rerun_replaces_partition_and_keeps_other_dates(db_path, csv_path): + make_op(csv_path, "2026-03-01").execute(context={}) + make_op(csv_path, "2026-03-02").execute(context={}) + assert make_op(csv_path, "2026-03-02").execute(context={}) == 2 + assert rows(db_path, "select order_id from raw_test.orders order by 1") == [("A1",), ("A2",), ("A3",)] diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/return_value_only/tests/test_warehouse.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/return_value_only/tests/test_warehouse.py new file mode 100644 index 0000000..2f39809 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/return_value_only/tests/test_warehouse.py @@ -0,0 +1,25 @@ +"""Smoke test: the operator runs against a temp DuckDB file and reports the row count.""" + +from __future__ import annotations + +import json + +from warehouse.operators import CsvPartitionToDuckDBOperator + + +def test_execute_returns_loaded_rows(tmp_path, monkeypatch): + monkeypatch.setenv( + "AIRFLOW_CONN_TEST_WH", + json.dumps({"conn_type": "duckdb", "host": str(tmp_path / "wh.duckdb"), "schema": "raw"}), + ) + csv_path = tmp_path / "orders.csv" + csv_path.write_text("order_id,order_date\nA1,2026-03-01\nA2,2026-03-02\nA3,2026-03-02\n") + op = CsvPartitionToDuckDBOperator( + task_id="load", + csv_path=str(csv_path), + table="orders", + partition_column="order_date", + partition_date="2026-03-02", + duckdb_conn_id="test_wh", + ) + assert op.execute(context={}) == 2 diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/_DELETE b/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/_DELETE new file mode 100644 index 0000000..fe5a617 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/_DELETE @@ -0,0 +1,3 @@ +# pure snapshot suite: drop the reference tests +tests/test_warehouse.py +tests/conftest.py diff --git a/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..8d66ba7 --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/mutants/source_snapshot/tests/test_dag_sources.py @@ -0,0 +1,22 @@ +"""Golden-file regression guard: fail CI when any DAG source drifts from the reviewed version.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + "load_raw_sales.py": "38a00af47f57d1e3058650b55f01702a250019a863174ea3c7f7067938acffaf", + "warehouse/__init__.py": "c45b2f9de5a495e409c73b74f4717935d091c74c936c14aded16f35c4ec23abd", + "warehouse/hooks.py": "a9b2fc0f94136cdc65a63b48b4186c65798360f498c16d4aa497adf9fae87799", + "warehouse/operators.py": "8c642d9c439ab0cc52bc41b00b3458f8f6b8ec9b4d3688ebffb1b80615acdc42", +} + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_source_matches_reviewed_version(rel): + digest = hashlib.sha256((DAGS_DIR / rel).read_bytes()).hexdigest() + assert digest == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-custom-operator-conn/reference/tests/conftest.py b/evals/airflow/cases/testing-custom-operator-conn/reference/tests/conftest.py new file mode 100644 index 0000000..8b4f28d --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/reference/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + +# Airflow puts the DAGs folder on sys.path when it parses DAGs; do the same for tests. +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-custom-operator-conn/reference/tests/test_warehouse.py b/evals/airflow/cases/testing-custom-operator-conn/reference/tests/test_warehouse.py new file mode 100644 index 0000000..0a53dca --- /dev/null +++ b/evals/airflow/cases/testing-custom-operator-conn/reference/tests/test_warehouse.py @@ -0,0 +1,83 @@ +"""Unit tests for the DuckDB warehouse hook and CSV partition loader. + +Connections come from AIRFLOW_CONN_* env vars, so no metadata DB is needed. +""" + +from __future__ import annotations + +import json + +import duckdb +import pytest +from warehouse.hooks import DuckDBWarehouseHook +from warehouse.operators import CsvPartitionToDuckDBOperator + +CONN_ID = "test_wh" + + +@pytest.fixture +def db_path(tmp_path, monkeypatch): + path = tmp_path / "wh.duckdb" + conn = {"conn_type": "duckdb", "host": str(path), "schema": "raw_test"} + monkeypatch.setenv(f"AIRFLOW_CONN_{CONN_ID.upper()}", json.dumps(conn)) + return path + + +@pytest.fixture +def csv_path(tmp_path): + path = tmp_path / "orders.csv" + path.write_text( + "order_id,order_date,amount\n" + "A1,2026-03-01,10.00\n" + "A2,2026-03-02,20.00\n" + "A3,2026-03-02,30.00\n" + "A4,2026-03-03,40.00\n" + ) + return path + + +def make_op(csv_path, day, conn_id=CONN_ID): + return CsvPartitionToDuckDBOperator( + task_id="load", + csv_path=str(csv_path), + table="orders", + partition_column="order_date", + partition_date=day, + duckdb_conn_id=conn_id, + ) + + +def rows(db_path, sql): + con = duckdb.connect(str(db_path), read_only=True) + try: + return con.execute(sql).fetchall() + finally: + con.close() + + +def test_hook_reads_connection_fields(db_path): + hook = DuckDBWarehouseHook(duckdb_conn_id=CONN_ID) + assert hook.database_path == str(db_path) + assert hook.target_schema == "raw_test" + con = hook.get_conn() + con.execute("create table t as select 1 as x") + con.close() + assert rows(db_path, "select x from t") == [(1,)] + + +def test_loads_only_the_partition_into_connection_schema(db_path, csv_path): + loaded = make_op(csv_path, "2026-03-02").execute(context={}) + assert loaded == 2 + assert rows(db_path, "select order_id from raw_test.orders order by 1") == [("A2",), ("A3",)] + + +def test_rerun_replaces_partition_and_keeps_other_dates(db_path, csv_path): + make_op(csv_path, "2026-03-01").execute(context={}) + make_op(csv_path, "2026-03-02").execute(context={}) + assert make_op(csv_path, "2026-03-02").execute(context={}) == 2 + assert rows(db_path, "select order_id from raw_test.orders order by 1") == [("A1",), ("A2",), ("A3",)] + + +def test_unknown_connection_fails(db_path, csv_path): + with pytest.raises(Exception, match="missing_conn"): + make_op(csv_path, "2026-03-02", conn_id="missing_conn").execute(context={}) diff --git a/evals/airflow/cases/testing-dagbag-integrity/alt_reference/tests/test_dags.py b/evals/airflow/cases/testing-dagbag-integrity/alt_reference/tests/test_dags.py new file mode 100644 index 0000000..3240006 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/alt_reference/tests/test_dags.py @@ -0,0 +1,51 @@ +import importlib.util +import os +from pathlib import Path + +import pytest +from airflow.models import DagBag + +DAG_FOLDER = Path(__file__).parent.parent / "dags" +DAG_FILES = sorted(DAG_FOLDER.glob("*.py")) + +os.environ.setdefault("AIRFLOW__CORE__LOAD_EXAMPLES", "False") +BAG = DagBag(dag_folder=str(DAG_FOLDER)) + + +@pytest.mark.parametrize("path", DAG_FILES, ids=lambda p: p.name) +def test_file_imports(path): + spec = importlib.util.spec_from_file_location(f"dagfile_{path.stem}", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + +def test_dagbag_has_no_errors(): + assert not BAG.import_errors, BAG.import_errors + assert len(BAG.dags) == 3 + + +@pytest.mark.parametrize("dag_id", sorted(BAG.dag_ids)) +class TestPolicy: + def test_tags(self, dag_id): + assert BAG.dags[dag_id].tags + + def test_catchup_off(self, dag_id): + assert BAG.dags[dag_id].catchup is False + + def test_owner(self, dag_id): + for t in BAG.dags[dag_id].tasks: + assert t.owner not in (None, "", "airflow"), t.task_id + + def test_retries(self, dag_id): + for t in BAG.dags[dag_id].tasks: + assert t.retries >= 2, f"{t.task_id} has retries={t.retries}" + + +def test_orders_ingest_dependencies(): + dag = BAG.dags["orders_ingest"] + edges = {(u.task_id, d.task_id) for u in dag.tasks for d in u.downstream_list} + assert edges == { + ("extract_orders", "validate_orders"), + ("validate_orders", "load_orders"), + ("load_orders", "publish_summary"), + } diff --git a/evals/airflow/cases/testing-dagbag-integrity/case.yaml b/evals/airflow/cases/testing-dagbag-integrity/case.yaml new file mode 100644 index 0000000..448b61a --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/case.yaml @@ -0,0 +1,33 @@ +id: testing-dagbag-integrity +area: testing +split: dev +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1800 +prompt: | + Last week someone merged a DAG into this repo that didn't even import, and we only + found out when it vanished from the Airflow UI. We have zero tests right now. Can + you add a pytest suite under tests/ that our CI can run with `pytest tests/`? CI is + just this Python venv (Airflow 3.3) - no scheduler, webserver or other services. + + The suite should fail when: + - any file in dags/ fails to load (import errors, cycles, anything that stops the + DAG from parsing) + - a DAG breaks our policy: every DAG needs at least one tag and catchup turned + off, and every task needs a real owner (not the default "airflow") and at least + 2 retries + - the orders_ingest pipeline order changes: it has to be + extract_orders -> validate_orders -> load_orders -> publish_summary + + It needs to pass on the current code. Please don't change the DAGs themselves. +description: | + Area: testing (Airflow 3.3). Pain point P5 (agents do not write real DAG tests) + plus the 3.3 DagBag API move: DagBag now lives in airflow.dag_processing.dagbag and + no longer accepts include_examples, so Airflow-2-era integrity tests die with a + TypeError. Primary checks: the agent's suite passes on the correct project and + fails behaviourally (not by collection error) on five hidden planted bugs the agent + never sees: a removed-module import error, a dependency cycle, a missing + dependency, a task-level retries=0 override (only visible per task, not in + default_args) and catchup=True. Mutants: Airflow-2 DagBag(include_examples=False), + policy checked on default_args only, and an import-errors-only suite. diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/.gitignore b/evals/airflow/cases/testing-dagbag-integrity/fixture/.gitignore new file mode 100644 index 0000000..1ac42ff --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/.gitignore @@ -0,0 +1,4 @@ +output/ +warehouse.duckdb +__pycache__/ +.pytest_cache/ diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/README.md b/evals/airflow/cases/testing-dagbag-integrity/fixture/README.md new file mode 100644 index 0000000..3607c62 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/README.md @@ -0,0 +1,14 @@ +# acme-data-platform + +Airflow 3.3 DAGs for the analytics warehouse (local DuckDB file in dev). + +- `dags/` - DAG files. +- `data/` - sample exports used in dev. +- `output/` and `warehouse.duckdb` are created by the DAGs and are git-ignored. + +Local run: + +```bash +pip install -r requirements.txt +airflow dags test orders_ingest 2026-03-02 +``` diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/customer_snapshot.py b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/customer_snapshot.py new file mode 100644 index 0000000..a59a920 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/customer_snapshot.py @@ -0,0 +1,52 @@ +"""Weekly snapshot of the customer dimension.""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +def _snapshot_customers(ds: str) -> int: + with (DATA_DIR / "customers.csv").open(newline="") as fh: + rows = list(csv.DictReader(fh)) + out = OUTPUT_DIR / "customer_snapshot" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=["customer_id", "name", "segment"]) + writer.writeheader() + writer.writerows(rows) + return len(rows) + + +with DAG( + dag_id="customer_snapshot", + schedule="0 4 * * 1", + start_date=datetime(2026, 1, 5), + catchup=False, + default_args=DEFAULT_ARGS, + tags=["customers"], +): + start = EmptyOperator(task_id="start") + snapshot_customers = PythonOperator( + task_id="snapshot_customers", + python_callable=_snapshot_customers, + op_kwargs={"ds": "{{ ds }}"}, + ) + done = EmptyOperator(task_id="done") + + start >> snapshot_customers >> done diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/marketing_spend.py b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/marketing_spend.py new file mode 100644 index 0000000..35d3237 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/marketing_spend.py @@ -0,0 +1,54 @@ +"""Daily marketing spend per channel, from the ad platforms' CSV drop.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +@dag( + schedule="30 5 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + default_args=DEFAULT_ARGS, + tags=["marketing"], +) +def marketing_spend(): + # The ad platforms publish late sometimes; give this one more room. + @task(retries=4, retry_delay=timedelta(minutes=15)) + def fetch_spend(ds=None) -> list[dict]: + with (DATA_DIR / "marketing_spend.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["spend_date"] == ds] + + @task + def spend_by_channel(rows: list[dict], ds=None) -> str: + totals: dict[str, float] = defaultdict(float) + for row in rows: + totals[row["channel"]] += float(row["spend_usd"]) + out = OUTPUT_DIR / "marketing_spend" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["channel", "spend_usd"]) + for channel in sorted(totals): + writer.writerow([channel, f"{totals[channel]:.2f}"]) + return str(out) + + spend_by_channel(fetch_spend()) + + +marketing_spend() diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/orders_ingest.py b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/orders_ingest.py new file mode 100644 index 0000000..d099d87 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/dags/orders_ingest.py @@ -0,0 +1,74 @@ +"""Load the daily orders export into the warehouse and publish a summary file.""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" +WAREHOUSE_PATH = PROJECT_ROOT / "warehouse.duckdb" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +@dag( + schedule="0 2 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["orders", "ingest"], +) +def orders_ingest(): + @task + def extract_orders(ds=None) -> list[dict]: + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + @task + def validate_orders(rows: list[dict]) -> list[dict]: + bad = [r["order_id"] for r in rows if not r["order_id"] or float(r["amount"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + @task + def load_orders(rows: list[dict], ds=None) -> int: + import duckdb + + con = duckdb.connect(str(WAREHOUSE_PATH)) + try: + con.execute( + "CREATE TABLE IF NOT EXISTS orders " + "(order_id VARCHAR, order_date DATE, customer_id VARCHAR, amount DOUBLE)" + ) + con.execute("DELETE FROM orders WHERE order_date = ?", [ds]) + con.executemany( + "INSERT INTO orders VALUES (?, ?, ?, ?)", + [[r["order_id"], r["order_date"], r["customer_id"], float(r["amount"])] for r in rows], + ) + finally: + con.close() + return len(rows) + + @task + def publish_summary(loaded: int, ds=None) -> str: + out = OUTPUT_DIR / "orders_summary" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"date": ds, "orders_loaded": loaded})) + return str(out) + + publish_summary(load_orders(validate_orders(extract_orders()))) + + +orders_ingest() diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/data/customers.csv b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/customers.csv new file mode 100644 index 0000000..17d6d8f --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/customers.csv @@ -0,0 +1,5 @@ +customer_id,name,segment +C-01,Northwind,enterprise +C-02,Globex,smb +C-03,Initech,smb +C-04,Umbrella,enterprise diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/data/marketing_spend.csv b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/marketing_spend.csv new file mode 100644 index 0000000..3614fb5 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/marketing_spend.csv @@ -0,0 +1,6 @@ +spend_date,channel,campaign,spend_usd +2026-03-01,search,brand,410.00 +2026-03-01,social,spring_promo,125.40 +2026-03-02,search,brand,388.20 +2026-03-02,search,generic,97.10 +2026-03-02,social,spring_promo,140.00 diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/data/orders.csv b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/orders.csv new file mode 100644 index 0000000..25b38da --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/data/orders.csv @@ -0,0 +1,7 @@ +order_id,order_date,customer_id,amount +O-1001,2026-03-01,C-01,120.50 +O-1002,2026-03-01,C-02,35.00 +O-1003,2026-03-02,C-01,18.25 +O-1004,2026-03-02,C-03,240.00 +O-1005,2026-03-02,C-04,9.99 +O-1006,2026-03-03,C-02,61.10 diff --git a/evals/airflow/cases/testing-dagbag-integrity/fixture/requirements.txt b/evals/airflow/cases/testing-dagbag-integrity/fixture/requirements.txt new file mode 100644 index 0000000..b76fe32 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/fixture/requirements.txt @@ -0,0 +1,2 @@ +apache-airflow==3.3.2 +duckdb>=1.1 diff --git a/evals/airflow/cases/testing-dagbag-integrity/grade.py b/evals/airflow/cases/testing-dagbag-integrity/grade.py new file mode 100644 index 0000000..0549004 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/grade.py @@ -0,0 +1,175 @@ +"""Grader for testing-dagbag-integrity (Airflow 3.3). + +The agent writes a pytest suite. Grading runs that suite, never the agent's claims: + +1. Against the correct project (the pristine fixture sources restored over the + agent's workspace): the suite must collect at least one test and pass. +2. Against each hidden planted-bug variant (``hidden_bugs//`` overlays the + agent never sees): at least one test must fail for a behavioural reason. A + test-module collection error does not count. For planted import errors, a + test that fails at run time because the DAG file cannot be imported does count + (that is exactly what a loader test is for), and so does a collection error + (a test module that imports the DAG files at collection time). +3. Against a behaviour-preserving edit of the sources (a comment added at the top + and bottom of every ``dags/**/*.py`` plus an unused ``__review_note__`` module + attribute): the suite must still pass. This rejects snapshot suites (hashing or + diffing the DAG files or their AST) that "catch" every planted bug without + testing any behaviour. + +Each pytest run gets its own AIRFLOW_HOME with a migrated sqlite metadata DB (what a +CI job has after ``airflow db migrate``); no scheduler or other services run. +The hidden-bug and pristine-restore logic is shared by every ``testing-*`` case. +""" + +from __future__ import annotations + +import concurrent.futures as cf +import filecmp +import os +import shutil +import sys +import tempfile +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +# Fixture paths restored before running the agent's suite (the code under test). +SOURCE_PATHS = ("dags", "data") +# name -> (what the planted bug is, whether it is an import-time failure) +HIDDEN_BUGS = { + "import_error": ("customer_snapshot imports the removed airflow.operators.dummy module", True), + "dag_cycle": ("orders_ingest gains a publish_summary >> extract_orders edge (cycle)", False), + "missing_dependency": ("orders_ingest loads straight from extract; validate_orders is left dangling", False), + "retries_disabled": ("marketing_spend.fetch_spend overrides retries=0 at task level", False), + "catchup_enabled": ("customer_snapshot sets catchup=True", False), +} +PYTEST_TIMEOUT = 300 +BENIGN_HEADER = "# Reviewed in PR: formatting only, behaviour unchanged.\n" +# A no-op module attribute as well as comments, so a suite that snapshots the code +# (AST or bytecode) rather than the text still counts as a snapshot suite. +BENIGN_FOOTER = '\n\n# End of module.\n__review_note__ = "formatting only, behaviour unchanged"\n' + + +def pristine_overlay(tmp: Path) -> Path: + """Overlay holding the fixture's source files (tests and configs excluded).""" + ov = tmp / "pristine" + for rel in SOURCE_PATHS: + src = CASE_DIR / "fixture" / rel + if src.is_dir(): + shutil.copytree(src, ov / rel, ignore=shutil.ignore_patterns("__pycache__")) + elif src.exists(): + (ov / rel).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, ov / rel) + return ov + + +def benign_overlay(tmp: Path) -> Path: + """Overlay with every fixture ``dags/**/*.py`` wrapped in comments plus a no-op attribute.""" + ov = tmp / "benign" + root = CASE_DIR / "fixture" + for src in (root / "dags").rglob("*.py"): + if "__pycache__" in src.parts: + continue + dst = ov / src.relative_to(root) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(BENIGN_HEADER + src.read_text().rstrip("\n") + BENIGN_FOOTER) + return ov + + +def migrated_home(py: str, tmp: Path) -> Path: + home = tmp / "af-home-template" + res = g.ensure_db(py, g.airflow_env(tmp, airflow_home=home, env_py=py)) + if not res.ok: + raise RuntimeError(f"airflow db migrate failed: {res.tail()}") + return home + + +def run_suite(py: str, ws: Path, home_template: Path) -> g.PytestResult: + home = Path(tempfile.mkdtemp(prefix="eval-af-home-")) + shutil.copytree(home_template, home, dirs_exist_ok=True) + env = g.airflow_env(ws, airflow_home=home, env_py=py) + return g.run_pytest(py, ws, ("tests",), env=env, timeout=PYTEST_TIMEOUT, + extra_args=("--continue-on-collection-errors",)) + + +def caught(res: g.PytestResult, import_kind: bool) -> bool: + if res.status == "timeout": + return False + if import_kind: + # Only the planted DAG file differs from the correct project (where the suite + # passed), so a collection error here is the suite importing that broken file: + # `pytest tests/` fails in CI, which is what the requirement asks for. + return res.status == "collection_error" or any( + t.outcome in ("failed", "error") and t.phase != "collection" for t in res.tests) + return bool(res.behavioral_failures) + + +def test_files(ws: Path) -> list[Path]: + tests = ws / "tests" + if not tests.is_dir(): + return [] + return sorted(set(tests.rglob("test_*.py")) | set(tests.rglob("*_test.py"))) + + +def changed_sources(ws: Path) -> list[str]: + changed = [] + for rel in SOURCE_PATHS: + root = CASE_DIR / "fixture" / rel + files = [p for p in root.rglob("*") if p.is_file()] if root.is_dir() else [root] + for f in files: + r = f.relative_to(CASE_DIR / "fixture") + if "__pycache__" in r.parts: + continue + if not (ws / r).is_file() or not filecmp.cmp(f, ws / r, shallow=False): + changed.append(str(r)) + return changed + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + tmp = Path(tempfile.mkdtemp(prefix="eval-testing-")) + + files = test_files(ws) + grader.primary("test suite exists under tests/", bool(files), + ", ".join(str(f.relative_to(ws)) for f in files) or "no test_*.py files under tests/") + + home = migrated_home(py, tmp) + correct = g.copy_workspace(ws, [pristine_overlay(tmp)]) + base = run_suite(py, correct, home) + base_ok = base.status == "passed" and base.passed >= 1 + grader.primary("suite passes on the correct project", base_ok, + base.summary() + ("" if base_ok else "\n" + base.output_tail[-2500:])) + + benign_ov = benign_overlay(tmp) + + def job(name: str): + overlay = benign_ov if name == "__benign__" else CASE_DIR / "hidden_bugs" / name + return name, run_suite(py, g.copy_workspace(correct, [overlay]), home) + + with cf.ThreadPoolExecutor(max_workers=3) as ex: + results = dict(ex.map(job, ["__benign__", *HIDDEN_BUGS])) + benign = results.pop("__benign__") + benign_ok = base_ok and benign.status == "passed" and benign.passed >= 1 + grader.primary("suite passes on a behaviour-preserving edit of the sources", benign_ok, + ("" if base_ok else "not attributable: the suite does not pass on the correct project; ") + + benign.summary() + ("" if benign_ok or not base_ok else "\n" + benign.output_tail[-1500:])) + for name, (what, import_kind) in HIDDEN_BUGS.items(): + res = results[name] + hit = base_ok and caught(res, import_kind) + prefix = "" if base_ok else "not attributable: the suite does not pass on the correct project; " + grader.primary(f"suite catches hidden bug: {name}", hit, f"{prefix}{what} -> {res.summary()}") + + changed = changed_sources(ws) + grader.secondary("project sources left unchanged", not changed, ", ".join(changed[:20])) + g.standard_secondary_checks(grader, py, correct, events, verify_patterns=(r"\bpytest\b",), + ruff_paths=("tests",) if files else ("dags",)) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/catchup_enabled/dags/customer_snapshot.py b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/catchup_enabled/dags/customer_snapshot.py new file mode 100644 index 0000000..969379f --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/catchup_enabled/dags/customer_snapshot.py @@ -0,0 +1,52 @@ +"""Weekly snapshot of the customer dimension.""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.providers.standard.operators.empty import EmptyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +def _snapshot_customers(ds: str) -> int: + with (DATA_DIR / "customers.csv").open(newline="") as fh: + rows = list(csv.DictReader(fh)) + out = OUTPUT_DIR / "customer_snapshot" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=["customer_id", "name", "segment"]) + writer.writeheader() + writer.writerows(rows) + return len(rows) + + +with DAG( + dag_id="customer_snapshot", + schedule="0 4 * * 1", + start_date=datetime(2026, 1, 5), + catchup=True, + default_args=DEFAULT_ARGS, + tags=["customers"], +): + start = EmptyOperator(task_id="start") + snapshot_customers = PythonOperator( + task_id="snapshot_customers", + python_callable=_snapshot_customers, + op_kwargs={"ds": "{{ ds }}"}, + ) + done = EmptyOperator(task_id="done") + + start >> snapshot_customers >> done diff --git a/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/dag_cycle/dags/orders_ingest.py b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/dag_cycle/dags/orders_ingest.py new file mode 100644 index 0000000..a6092e8 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/dag_cycle/dags/orders_ingest.py @@ -0,0 +1,77 @@ +"""Load the daily orders export into the warehouse and publish a summary file.""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" +WAREHOUSE_PATH = PROJECT_ROOT / "warehouse.duckdb" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +@dag( + schedule="0 2 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["orders", "ingest"], +) +def orders_ingest(): + @task + def extract_orders(ds=None) -> list[dict]: + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + @task + def validate_orders(rows: list[dict]) -> list[dict]: + bad = [r["order_id"] for r in rows if not r["order_id"] or float(r["amount"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + @task + def load_orders(rows: list[dict], ds=None) -> int: + import duckdb + + con = duckdb.connect(str(WAREHOUSE_PATH)) + try: + con.execute( + "CREATE TABLE IF NOT EXISTS orders " + "(order_id VARCHAR, order_date DATE, customer_id VARCHAR, amount DOUBLE)" + ) + con.execute("DELETE FROM orders WHERE order_date = ?", [ds]) + con.executemany( + "INSERT INTO orders VALUES (?, ?, ?, ?)", + [[r["order_id"], r["order_date"], r["customer_id"], float(r["amount"])] for r in rows], + ) + finally: + con.close() + return len(rows) + + @task + def publish_summary(loaded: int, ds=None) -> str: + out = OUTPUT_DIR / "orders_summary" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"date": ds, "orders_loaded": loaded})) + return str(out) + + extracted = extract_orders() + summary = publish_summary(load_orders(validate_orders(extracted))) + # re-extract once the summary is out so late orders are picked up + summary >> extracted + + +orders_ingest() diff --git a/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/import_error/dags/customer_snapshot.py b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/import_error/dags/customer_snapshot.py new file mode 100644 index 0000000..5f756df --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/import_error/dags/customer_snapshot.py @@ -0,0 +1,52 @@ +"""Weekly snapshot of the customer dimension.""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.operators.dummy import DummyOperator +from airflow.providers.standard.operators.python import PythonOperator +from airflow.sdk import DAG + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +def _snapshot_customers(ds: str) -> int: + with (DATA_DIR / "customers.csv").open(newline="") as fh: + rows = list(csv.DictReader(fh)) + out = OUTPUT_DIR / "customer_snapshot" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=["customer_id", "name", "segment"]) + writer.writeheader() + writer.writerows(rows) + return len(rows) + + +with DAG( + dag_id="customer_snapshot", + schedule="0 4 * * 1", + start_date=datetime(2026, 1, 5), + catchup=False, + default_args=DEFAULT_ARGS, + tags=["customers"], +): + start = DummyOperator(task_id="start") + snapshot_customers = PythonOperator( + task_id="snapshot_customers", + python_callable=_snapshot_customers, + op_kwargs={"ds": "{{ ds }}"}, + ) + done = DummyOperator(task_id="done") + + start >> snapshot_customers >> done diff --git a/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/missing_dependency/dags/orders_ingest.py b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/missing_dependency/dags/orders_ingest.py new file mode 100644 index 0000000..bc4ef4e --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/missing_dependency/dags/orders_ingest.py @@ -0,0 +1,76 @@ +"""Load the daily orders export into the warehouse and publish a summary file.""" + +from __future__ import annotations + +import csv +import json +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" +WAREHOUSE_PATH = PROJECT_ROOT / "warehouse.duckdb" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +@dag( + schedule="0 2 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args=DEFAULT_ARGS, + tags=["orders", "ingest"], +) +def orders_ingest(): + @task + def extract_orders(ds=None) -> list[dict]: + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + @task + def validate_orders(rows: list[dict]) -> list[dict]: + bad = [r["order_id"] for r in rows if not r["order_id"] or float(r["amount"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + @task + def load_orders(rows: list[dict], ds=None) -> int: + import duckdb + + con = duckdb.connect(str(WAREHOUSE_PATH)) + try: + con.execute( + "CREATE TABLE IF NOT EXISTS orders " + "(order_id VARCHAR, order_date DATE, customer_id VARCHAR, amount DOUBLE)" + ) + con.execute("DELETE FROM orders WHERE order_date = ?", [ds]) + con.executemany( + "INSERT INTO orders VALUES (?, ?, ?, ?)", + [[r["order_id"], r["order_date"], r["customer_id"], float(r["amount"])] for r in rows], + ) + finally: + con.close() + return len(rows) + + @task + def publish_summary(loaded: int, ds=None) -> str: + out = OUTPUT_DIR / "orders_summary" / f"{ds}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps({"date": ds, "orders_loaded": loaded})) + return str(out) + + extracted = extract_orders() + validate_orders(extracted) + publish_summary(load_orders(extracted)) + + +orders_ingest() diff --git a/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/retries_disabled/dags/marketing_spend.py b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/retries_disabled/dags/marketing_spend.py new file mode 100644 index 0000000..84bd2ce --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/hidden_bugs/retries_disabled/dags/marketing_spend.py @@ -0,0 +1,54 @@ +"""Daily marketing spend per channel, from the ad platforms' CSV drop.""" + +from __future__ import annotations + +import csv +from collections import defaultdict +from datetime import datetime, timedelta +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" + +DEFAULT_ARGS = { + "owner": "data-platform", + "retries": 2, + "retry_delay": timedelta(minutes=5), +} + + +@dag( + schedule="30 5 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + default_args=DEFAULT_ARGS, + tags=["marketing"], +) +def marketing_spend(): + # Retrying re-pulls the whole drop and double counts, so fail fast instead. + @task(retries=0) + def fetch_spend(ds=None) -> list[dict]: + with (DATA_DIR / "marketing_spend.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["spend_date"] == ds] + + @task + def spend_by_channel(rows: list[dict], ds=None) -> str: + totals: dict[str, float] = defaultdict(float) + for row in rows: + totals[row["channel"]] += float(row["spend_usd"]) + out = OUTPUT_DIR / "marketing_spend" / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.writer(fh) + writer.writerow(["channel", "spend_usd"]) + for channel in sorted(totals): + writer.writerow([channel, f"{totals[channel]:.2f}"]) + return str(out) + + spend_by_channel(fetch_spend()) + + +marketing_spend() diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/airflow2_dagbag/tests/test_dag_integrity.py b/evals/airflow/cases/testing-dagbag-integrity/mutants/airflow2_dagbag/tests/test_dag_integrity.py new file mode 100644 index 0000000..a06aa90 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/airflow2_dagbag/tests/test_dag_integrity.py @@ -0,0 +1,51 @@ +"""DAG integrity and policy checks for every DAG in dags/. Runs without a scheduler.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from airflow.models import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +EXPECTED_DAGS = {"orders_ingest", "customer_snapshot", "marketing_spend"} + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + return DagBag(dag_folder=str(DAGS_DIR), include_examples=False) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, "\n".join( + f"{path}:\n{err}" for path, err in dagbag.import_errors.items() + ) + + +def test_expected_dags_loaded(dagbag): + assert EXPECTED_DAGS <= set(dagbag.dag_ids) + + +def test_every_dag_follows_policy(dagbag): + assert dagbag.dags, "no DAGs parsed" + problems = [] + for dag_id, dag in dagbag.dags.items(): + if not dag.tags: + problems.append(f"{dag_id}: no tags") + if dag.catchup: + problems.append(f"{dag_id}: catchup is enabled") + for task in dag.tasks: + if not task.owner or task.owner == "airflow": + problems.append(f"{dag_id}.{task.task_id}: owner not set") + if (task.retries or 0) < 2: + problems.append(f"{dag_id}.{task.task_id}: retries={task.retries}") + assert not problems, problems + + +def test_orders_ingest_shape(dagbag): + dag = dagbag.dags["orders_ingest"] + chain = ["extract_orders", "validate_orders", "load_orders", "publish_summary"] + assert set(dag.task_ids) == set(chain) + for upstream, downstream in zip(chain, chain[1:]): + assert dag.get_task(downstream).upstream_task_ids == {upstream} + assert dag.get_task("extract_orders").upstream_task_ids == set() diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/_DELETE b/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/_DELETE new file mode 100644 index 0000000..252106a --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/_DELETE @@ -0,0 +1,2 @@ +# pure snapshot suite: drop the reference tests +tests/test_dag_integrity.py diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..aa5a064 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/ast_snapshot/tests/test_dag_sources.py @@ -0,0 +1,29 @@ +"""Golden-AST regression guard: fail CI when the code of any DAG file changes. + +Comments and formatting are ignored (the AST is compared), so reformatting or +documenting a DAG does not trip it; any change to the code does. +""" + +from __future__ import annotations + +import ast +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + 'customer_snapshot.py': '700c38701f2408558b66c95b5b230ae7435688a229422f43ac4bdbaab0a344d4', + 'marketing_spend.py': '65b8b329717e530fa461f52660e577681c28fb2dc6ab3f9f4c5d50d8d35261a1', + 'orders_ingest.py': '3e542e10fe7b98e5550e1bb446ec98a8f983ebb27c54abb8382d6d9f9547d0c1', +} + + +def _ast_digest(path: Path) -> str: + return hashlib.sha256(ast.dump(ast.parse(path.read_text())).encode()).hexdigest() + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_code_matches_reviewed_version(rel): + assert _ast_digest(DAGS_DIR / rel) == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/default_args_retries_only/tests/test_dag_integrity.py b/evals/airflow/cases/testing-dagbag-integrity/mutants/default_args_retries_only/tests/test_dag_integrity.py new file mode 100644 index 0000000..49506c3 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/default_args_retries_only/tests/test_dag_integrity.py @@ -0,0 +1,53 @@ +"""DAG integrity and policy checks for every DAG in dags/. Runs without a scheduler.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from airflow.dag_processing.dagbag import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +EXPECTED_DAGS = {"orders_ingest", "customer_snapshot", "marketing_spend"} + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + # Airflow 3.x: DagBag lives in airflow.dag_processing.dagbag and has no + # include_examples argument (examples are controlled by core.load_examples). + return DagBag(dag_folder=str(DAGS_DIR)) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, "\n".join( + f"{path}:\n{err}" for path, err in dagbag.import_errors.items() + ) + + +def test_expected_dags_loaded(dagbag): + assert EXPECTED_DAGS <= set(dagbag.dag_ids) + + +def test_every_dag_follows_policy(dagbag): + assert dagbag.dags, "no DAGs parsed" + problems = [] + for dag_id, dag in dagbag.dags.items(): + if not dag.tags: + problems.append(f"{dag_id}: no tags") + if dag.catchup: + problems.append(f"{dag_id}: catchup is enabled") + args = dag.default_args or {} + if args.get("owner", "airflow") == "airflow": + problems.append(f"{dag_id}: owner not set") + if args.get("retries", 0) < 2: + problems.append(f"{dag_id}: retries={args.get('retries')}") + assert not problems, problems + + +def test_orders_ingest_shape(dagbag): + dag = dagbag.dags["orders_ingest"] + chain = ["extract_orders", "validate_orders", "load_orders", "publish_summary"] + assert set(dag.task_ids) == set(chain) + for upstream, downstream in zip(chain, chain[1:]): + assert dag.get_task(downstream).upstream_task_ids == {upstream} + assert dag.get_task("extract_orders").upstream_task_ids == set() diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/import_errors_only/tests/test_dag_integrity.py b/evals/airflow/cases/testing-dagbag-integrity/mutants/import_errors_only/tests/test_dag_integrity.py new file mode 100644 index 0000000..4bd1130 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/import_errors_only/tests/test_dag_integrity.py @@ -0,0 +1,21 @@ +"""Checks that every DAG in dags/ loads.""" + +from __future__ import annotations + +from pathlib import Path + +from airflow.dag_processing.dagbag import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" + + +def test_no_import_errors(): + bag = DagBag(dag_folder=str(DAGS_DIR)) + assert bag.import_errors == {} + + +def test_all_dags_present(): + bag = DagBag(dag_folder=str(DAGS_DIR)) + assert {"orders_ingest", "customer_snapshot", "marketing_spend"} <= set(bag.dag_ids) + for dag in bag.dags.values(): + assert dag.tasks diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/_DELETE b/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/_DELETE new file mode 100644 index 0000000..252106a --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/_DELETE @@ -0,0 +1,2 @@ +# pure snapshot suite: drop the reference tests +tests/test_dag_integrity.py diff --git a/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..371c70b --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/mutants/source_snapshot/tests/test_dag_sources.py @@ -0,0 +1,21 @@ +"""Golden-file regression guard: fail CI when any DAG source drifts from the reviewed version.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + "customer_snapshot.py": "b95530fd787b8316036d341675c74d52d2da715d7400c5a137b97d696fe0d0e6", + "marketing_spend.py": "9c2ab57505a057eb4ab9a143b4b316f60881a8d4ff123ba721ed664105dbfa7b", + "orders_ingest.py": "caee1cca563572392e2b7c94f8f6726dff1448caa2965c0cd11685730c27dbed", +} + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_source_matches_reviewed_version(rel): + digest = hashlib.sha256((DAGS_DIR / rel).read_bytes()).hexdigest() + assert digest == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-dagbag-integrity/reference/tests/test_dag_integrity.py b/evals/airflow/cases/testing-dagbag-integrity/reference/tests/test_dag_integrity.py new file mode 100644 index 0000000..70359f6 --- /dev/null +++ b/evals/airflow/cases/testing-dagbag-integrity/reference/tests/test_dag_integrity.py @@ -0,0 +1,53 @@ +"""DAG integrity and policy checks for every DAG in dags/. Runs without a scheduler.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from airflow.dag_processing.dagbag import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +EXPECTED_DAGS = {"orders_ingest", "customer_snapshot", "marketing_spend"} + + +@pytest.fixture(scope="session") +def dagbag() -> DagBag: + # Airflow 3.x: DagBag lives in airflow.dag_processing.dagbag and has no + # include_examples argument (examples are controlled by core.load_examples). + return DagBag(dag_folder=str(DAGS_DIR)) + + +def test_no_import_errors(dagbag): + assert dagbag.import_errors == {}, "\n".join( + f"{path}:\n{err}" for path, err in dagbag.import_errors.items() + ) + + +def test_expected_dags_loaded(dagbag): + assert EXPECTED_DAGS <= set(dagbag.dag_ids) + + +def test_every_dag_follows_policy(dagbag): + assert dagbag.dags, "no DAGs parsed" + problems = [] + for dag_id, dag in dagbag.dags.items(): + if not dag.tags: + problems.append(f"{dag_id}: no tags") + if dag.catchup: + problems.append(f"{dag_id}: catchup is enabled") + for task in dag.tasks: + if not task.owner or task.owner == "airflow": + problems.append(f"{dag_id}.{task.task_id}: owner not set") + if (task.retries or 0) < 2: + problems.append(f"{dag_id}.{task.task_id}: retries={task.retries}") + assert not problems, problems + + +def test_orders_ingest_shape(dagbag): + dag = dagbag.dags["orders_ingest"] + chain = ["extract_orders", "validate_orders", "load_orders", "publish_summary"] + assert set(dag.task_ids) == set(chain) + for upstream, downstream in zip(chain, chain[1:]): + assert dag.get_task(downstream).upstream_task_ids == {upstream} + assert dag.get_task("extract_orders").upstream_task_ids == set() diff --git a/evals/airflow/cases/testing-taskflow-transforms/alt_reference/tests/test_enrichment_pipeline.py b/evals/airflow/cases/testing-taskflow-transforms/alt_reference/tests/test_enrichment_pipeline.py new file mode 100644 index 0000000..4a59386 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/alt_reference/tests/test_enrichment_pipeline.py @@ -0,0 +1,95 @@ +import csv +import importlib.util +from datetime import datetime, timezone +from decimal import Decimal +from pathlib import Path + +import pytest +from airflow.models import DagBag +from airflow.timetables.base import TimeRestriction +from airflow.timetables.trigger import CronTriggerTimetable + +ROOT = Path(__file__).resolve().parent.parent +DAG_FILE = ROOT / "dags" / "orders_enrichment.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("orders_enrichment_under_test", DAG_FILE) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +enrichment = _load_module() + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(ROOT / "dags")) + assert not bag.import_errors + return bag.get_dag("orders_enrichment") + + +@pytest.mark.parametrize( + "amount,rate,expected", + [("0.35", "1.0850", "0.38"), ("40.50", "1.2700", "51.44"), ("10.00", "1", "10.00"), ("1.005", "1", "1.01")], +) +def test_usd_rounding_half_up(amount, rate, expected): + rows = [{"order_id": "X", "currency": "EUR", "amount": amount}] + (out,) = enrichment.convert_to_usd.function(rows, {"USD": "1", "EUR": rate}) + assert out["amount_usd"] == expected + + +def test_missing_rate_raises(): + with pytest.raises(ValueError, match="CHF"): + enrichment.convert_to_usd.function( + [{"order_id": "X", "currency": "CHF", "amount": "1.00"}], {"USD": "1"} + ) + + +def test_latest_version_wins_regardless_of_file_order(): + early = {"order_id": "O1", "updated_at": "2026-03-02T01:00:00", "amount": "1"} + late = {"order_id": "O1", "updated_at": "2026-03-02T23:00:00", "amount": "2"} + assert enrichment.dedupe_orders.function([early, late]) == [late] + assert enrichment.dedupe_orders.function([late, early]) == [late] + + +def test_negative_amount_is_rejected(): + with pytest.raises(ValueError): + enrichment.validate_totals.function([{"order_id": "X", "customer_id": "C", "amount_usd": "-1.00"}]) + + +def test_nothing_published_before_validation(dag): + assert dag.get_task("load_enriched").upstream_task_ids == {"validate_totals"} + assert dag.get_task("validate_totals").upstream_task_ids == {"convert_to_usd"} + + +def test_next_run_is_0600_utc(dag): + timetable = CronTriggerTimetable(dag.schedule, timezone="UTC") + info = timetable.next_dagrun_info( + last_automated_data_interval=None, + restriction=TimeRestriction(earliest=datetime(2026, 3, 2, tzinfo=timezone.utc), latest=None, catchup=True), + ) + assert info.run_after == datetime(2026, 3, 2, 6, tzinfo=timezone.utc) + later = timetable.next_dagrun_info( + last_automated_data_interval=info.data_interval, + restriction=TimeRestriction(earliest=None, latest=None, catchup=True), + ) + assert later.run_after == datetime(2026, 3, 3, 6, tzinfo=timezone.utc) + + +def test_sample_day_pipeline(tmp_path, monkeypatch): + """Run the task functions in DAG order on the sample data for 2026-03-02.""" + monkeypatch.setattr(enrichment, "OUTPUT_DIR", tmp_path) + ds = "2026-03-02" + orders = enrichment.dedupe_orders.function(enrichment.extract_orders.function(ds=ds)) + enriched = enrichment.convert_to_usd.function(orders, enrichment.load_fx_rates.function(ds=ds)) + path = enrichment.load_enriched.function(enrichment.validate_totals.function(enriched), ds=ds) + with open(path, newline="") as fh: + got = {r["order_id"]: Decimal(r["amount_usd"]) for r in csv.DictReader(fh)} + assert got == { + "S-102": Decimal("130.20"), + "S-103": Decimal("51.44"), + "S-104": Decimal("9.99"), + "S-105": Decimal("0.38"), + } diff --git a/evals/airflow/cases/testing-taskflow-transforms/case.yaml b/evals/airflow/cases/testing-taskflow-transforms/case.yaml new file mode 100644 index 0000000..d2c540f --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/case.yaml @@ -0,0 +1,37 @@ +id: testing-taskflow-transforms +area: testing +split: holdout +airflow_version: "3.3" +max_turns: 40 +timeout_s: 900 +grade_timeout_s: 1800 +prompt: | + Finance got a wrong USD number from orders_enrichment last month and nobody caught + it until month-end close. The DAG has no tests. Can you write a pytest suite under + tests/ for it? CI runs `pytest tests/` in this venv (Airflow 3.3) with nothing else + running, no scheduler or webserver. + + What finance relies on: + - when an order was exported more than once, only its latest version (by + updated_at) is kept + - amount_usd = amount * that day's usd_per_unit, rounded half-up to cents + - an order in a currency with no FX rate for the day has to fail the run, not get + guessed + - nothing gets written to output/ unless validate_totals has passed + - it runs once a day at 06:00 UTC + + I'd like the transform logic tested directly and also one end-to-end check on the + sample data in data/. The suite has to pass on the current code; please don't + change the DAG. +description: | + Area: testing (Airflow 3.3). Pain point P5 (agents write shallow or broken DAG + tests). TaskFlow functions must be tested through `.function` (calling the + decorated task returns an XComArg in 3.x), wiring checked on the parsed DAG, the + schedule checked without the removed `schedule_interval`, and an end-to-end run + without a scheduler (dag.test() or composing the functions). Primary checks: the + suite passes on the correct DAG and fails behaviourally on five hidden planted + bugs: dedupe keeps the first row instead of the latest, ROUND_DOWN instead of + half-up, unknown currency silently treated as USD, validate_totals no longer + upstream of load_enriched, and schedule changed to @daily. Mutants: tests that + call the @task objects directly, tests using dag.schedule_interval, and a + wiring/schedule-only suite that never exercises the transforms. diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/.gitignore b/evals/airflow/cases/testing-taskflow-transforms/fixture/.gitignore new file mode 100644 index 0000000..7364586 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/.gitignore @@ -0,0 +1,3 @@ +output/ +__pycache__/ +.pytest_cache/ diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/README.md b/evals/airflow/cases/testing-taskflow-transforms/fixture/README.md new file mode 100644 index 0000000..5667ae0 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/README.md @@ -0,0 +1,15 @@ +# finance-orders + +Airflow 3.3 pipeline that enriches the daily orders export with USD amounts for the +finance team. + +- `dags/orders_enrichment.py` - extract, dedupe, FX-convert, validate and publish + `output/orders_enriched/.csv`. +- `data/` - sample exports (orders and daily FX rates) used in dev. + +Local run: + +```bash +pip install -r requirements.txt +airflow dags test orders_enrichment 2026-03-02 +``` diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/fixture/dags/orders_enrichment.py new file mode 100644 index 0000000..9eacf18 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/dags/orders_enrichment.py @@ -0,0 +1,98 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None or row["updated_at"] > current["updated_at"]: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + rate = rates.get(row["currency"]) + if rate is None: + raise ValueError(f"no FX rate for {row['currency']} (order {row['order_id']})") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + load_enriched(validate_totals(enriched)) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/data/fx_rates.csv b/evals/airflow/cases/testing-taskflow-transforms/fixture/data/fx_rates.csv new file mode 100644 index 0000000..d2c8f27 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/data/fx_rates.csv @@ -0,0 +1,7 @@ +rate_date,currency,usd_per_unit +2026-03-01,EUR,1.0812 +2026-03-01,GBP,1.2655 +2026-03-02,EUR,1.0850 +2026-03-02,GBP,1.2700 +2026-03-03,EUR,1.0833 +2026-03-03,GBP,1.2690 diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/data/orders.csv b/evals/airflow/cases/testing-taskflow-transforms/fixture/data/orders.csv new file mode 100644 index 0000000..ca14a15 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/data/orders.csv @@ -0,0 +1,10 @@ +order_id,order_date,updated_at,customer_id,currency,amount +S-100,2026-03-01,2026-03-01T09:12:00,C-7,USD,15.00 +S-101,2026-03-01,2026-03-01T10:40:00,C-8,EUR,20.00 +S-102,2026-03-02,2026-03-02T08:05:00,C-7,EUR,100.00 +S-103,2026-03-02,2026-03-02T08:30:00,C-9,GBP,40.50 +S-102,2026-03-02,2026-03-02T11:47:00,C-7,EUR,120.00 +S-104,2026-03-02,2026-03-02T12:00:00,C-10,USD,9.99 +S-103,2026-03-02,2026-03-02T07:55:00,C-9,GBP,38.00 +S-105,2026-03-02,2026-03-02T13:20:00,C-8,EUR,0.35 +S-106,2026-03-03,2026-03-03T09:00:00,C-10,GBP,70.00 diff --git a/evals/airflow/cases/testing-taskflow-transforms/fixture/requirements.txt b/evals/airflow/cases/testing-taskflow-transforms/fixture/requirements.txt new file mode 100644 index 0000000..b1dd561 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/fixture/requirements.txt @@ -0,0 +1 @@ +apache-airflow==3.3.2 diff --git a/evals/airflow/cases/testing-taskflow-transforms/grade.py b/evals/airflow/cases/testing-taskflow-transforms/grade.py new file mode 100644 index 0000000..6f44482 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/grade.py @@ -0,0 +1,170 @@ +"""Grader for testing-taskflow-transforms (Airflow 3.3). + +The agent writes a pytest suite. Grading runs that suite, never the agent's claims: + +1. Against the correct project (the pristine fixture sources restored over the + agent's workspace): the suite must collect at least one test and pass. +2. Against each hidden planted-bug variant (``hidden_bugs//`` overlays the + agent never sees): at least one test must fail for a behavioural reason. A + test-module collection error does not count. For planted import errors, a + test that fails at run time because the DAG file cannot be imported does count + (that is exactly what a loader test is for). +3. Against a behaviour-preserving edit of the sources (a comment added at the top + and bottom of every ``dags/**/*.py`` plus an unused ``__review_note__`` module + attribute): the suite must still pass. This rejects snapshot suites (hashing or + diffing the DAG files or their AST) that "catch" every planted bug without + testing any behaviour. + +Each pytest run gets its own AIRFLOW_HOME with a migrated sqlite metadata DB (what a +CI job has after ``airflow db migrate``); no scheduler or other services run. +The hidden-bug and pristine-restore logic is shared by every ``testing-*`` case. +""" + +from __future__ import annotations + +import concurrent.futures as cf +import filecmp +import os +import shutil +import sys +import tempfile +from pathlib import Path + +CASE_DIR = Path(__file__).resolve().parent +sys.path.insert(0, os.environ.get("EVAL_HARNESS_DIR", str(CASE_DIR.parents[2] / "harness"))) +import grading as g # noqa: E402 + +# Fixture paths restored before running the agent's suite (the code under test). +SOURCE_PATHS = ("dags", "data") +# name -> (what the planted bug is, whether it is an import-time failure) +HIDDEN_BUGS = { + "dedupe_keeps_first": ("dedupe_orders keeps the first row seen per order_id instead of the latest updated_at", False), + "rounding_truncates": ("convert_to_usd rounds down (ROUND_DOWN) instead of half-up", False), + "unknown_currency_as_usd": ("convert_to_usd treats a currency with no FX rate as USD instead of failing", False), + "validation_bypassed": ("load_enriched is wired from convert_to_usd; validate_totals runs in parallel", False), + "schedule_midnight": ("schedule changed from 0 6 * * * to @daily (00:00 UTC)", False), +} +PYTEST_TIMEOUT = 300 +BENIGN_HEADER = "# Reviewed in PR: formatting only, behaviour unchanged.\n" +# A no-op module attribute as well as comments, so a suite that snapshots the code +# (AST or bytecode) rather than the text still counts as a snapshot suite. +BENIGN_FOOTER = '\n\n# End of module.\n__review_note__ = "formatting only, behaviour unchanged"\n' + + +def pristine_overlay(tmp: Path) -> Path: + """Overlay holding the fixture's source files (tests and configs excluded).""" + ov = tmp / "pristine" + for rel in SOURCE_PATHS: + src = CASE_DIR / "fixture" / rel + if src.is_dir(): + shutil.copytree(src, ov / rel, ignore=shutil.ignore_patterns("__pycache__")) + elif src.exists(): + (ov / rel).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, ov / rel) + return ov + + +def benign_overlay(tmp: Path) -> Path: + """Overlay with every fixture ``dags/**/*.py`` wrapped in comments plus a no-op attribute.""" + ov = tmp / "benign" + root = CASE_DIR / "fixture" + for src in (root / "dags").rglob("*.py"): + if "__pycache__" in src.parts: + continue + dst = ov / src.relative_to(root) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(BENIGN_HEADER + src.read_text().rstrip("\n") + BENIGN_FOOTER) + return ov + + +def migrated_home(py: str, tmp: Path) -> Path: + home = tmp / "af-home-template" + res = g.ensure_db(py, g.airflow_env(tmp, airflow_home=home, env_py=py)) + if not res.ok: + raise RuntimeError(f"airflow db migrate failed: {res.tail()}") + return home + + +def run_suite(py: str, ws: Path, home_template: Path) -> g.PytestResult: + home = Path(tempfile.mkdtemp(prefix="eval-af-home-")) + shutil.copytree(home_template, home, dirs_exist_ok=True) + env = g.airflow_env(ws, airflow_home=home, env_py=py) + return g.run_pytest(py, ws, ("tests",), env=env, timeout=PYTEST_TIMEOUT, + extra_args=("--continue-on-collection-errors",)) + + +def caught(res: g.PytestResult, import_kind: bool) -> bool: + if res.status == "timeout": + return False + if import_kind: + return any(t.outcome in ("failed", "error") and t.phase != "collection" for t in res.tests) + return bool(res.behavioral_failures) + + +def test_files(ws: Path) -> list[Path]: + tests = ws / "tests" + if not tests.is_dir(): + return [] + return sorted(set(tests.rglob("test_*.py")) | set(tests.rglob("*_test.py"))) + + +def changed_sources(ws: Path) -> list[str]: + changed = [] + for rel in SOURCE_PATHS: + root = CASE_DIR / "fixture" / rel + files = [p for p in root.rglob("*") if p.is_file()] if root.is_dir() else [root] + for f in files: + r = f.relative_to(CASE_DIR / "fixture") + if "__pycache__" in r.parts: + continue + if not (ws / r).is_file() or not filecmp.cmp(f, ws / r, shallow=False): + changed.append(str(r)) + return changed + + +def main() -> None: + args = g.parse_args() + ws, py = args.workspace, sys.executable + grader = g.Grader() + events = g.load_events(args.events) + tmp = Path(tempfile.mkdtemp(prefix="eval-testing-")) + + files = test_files(ws) + grader.primary("test suite exists under tests/", bool(files), + ", ".join(str(f.relative_to(ws)) for f in files) or "no test_*.py files under tests/") + + home = migrated_home(py, tmp) + correct = g.copy_workspace(ws, [pristine_overlay(tmp)]) + base = run_suite(py, correct, home) + base_ok = base.status == "passed" and base.passed >= 1 + grader.primary("suite passes on the correct project", base_ok, + base.summary() + ("" if base_ok else "\n" + base.output_tail[-2500:])) + + benign_ov = benign_overlay(tmp) + + def job(name: str): + overlay = benign_ov if name == "__benign__" else CASE_DIR / "hidden_bugs" / name + return name, run_suite(py, g.copy_workspace(correct, [overlay]), home) + + with cf.ThreadPoolExecutor(max_workers=3) as ex: + results = dict(ex.map(job, ["__benign__", *HIDDEN_BUGS])) + benign = results.pop("__benign__") + benign_ok = base_ok and benign.status == "passed" and benign.passed >= 1 + grader.primary("suite passes on a behaviour-preserving edit of the sources", benign_ok, + ("" if base_ok else "not attributable: the suite does not pass on the correct project; ") + + benign.summary() + ("" if benign_ok or not base_ok else "\n" + benign.output_tail[-1500:])) + for name, (what, import_kind) in HIDDEN_BUGS.items(): + res = results[name] + hit = base_ok and caught(res, import_kind) + prefix = "" if base_ok else "not attributable: the suite does not pass on the correct project; " + grader.primary(f"suite catches hidden bug: {name}", hit, f"{prefix}{what} -> {res.summary()}") + + changed = changed_sources(ws) + grader.secondary("project sources left unchanged", not changed, ", ".join(changed[:20])) + g.standard_secondary_checks(grader, py, correct, events, verify_patterns=(r"\bpytest\b",), + ruff_paths=("tests",) if files else ("dags",)) + grader.write(args.out) + + +if __name__ == "__main__": + main() diff --git a/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/dedupe_keeps_first/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/dedupe_keeps_first/dags/orders_enrichment.py new file mode 100644 index 0000000..4e98c1a --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/dedupe_keeps_first/dags/orders_enrichment.py @@ -0,0 +1,98 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + rate = rates.get(row["currency"]) + if rate is None: + raise ValueError(f"no FX rate for {row['currency']} (order {row['order_id']})") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + load_enriched(validate_totals(enriched)) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/rounding_truncates/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/rounding_truncates/dags/orders_enrichment.py new file mode 100644 index 0000000..2cfe278 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/rounding_truncates/dags/orders_enrichment.py @@ -0,0 +1,98 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_DOWN, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None or row["updated_at"] > current["updated_at"]: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + rate = rates.get(row["currency"]) + if rate is None: + raise ValueError(f"no FX rate for {row['currency']} (order {row['order_id']})") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_DOWN) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + load_enriched(validate_totals(enriched)) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/schedule_midnight/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/schedule_midnight/dags/orders_enrichment.py new file mode 100644 index 0000000..340eaa7 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/schedule_midnight/dags/orders_enrichment.py @@ -0,0 +1,98 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None or row["updated_at"] > current["updated_at"]: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + rate = rates.get(row["currency"]) + if rate is None: + raise ValueError(f"no FX rate for {row['currency']} (order {row['order_id']})") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="@daily", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + load_enriched(validate_totals(enriched)) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/unknown_currency_as_usd/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/unknown_currency_as_usd/dags/orders_enrichment.py new file mode 100644 index 0000000..77aa8d2 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/unknown_currency_as_usd/dags/orders_enrichment.py @@ -0,0 +1,97 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None or row["updated_at"] > current["updated_at"]: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + # Unmapped currency codes are almost always USD exports with a blank code. + rate = rates.get(row["currency"], "1") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + load_enriched(validate_totals(enriched)) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/validation_bypassed/dags/orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/validation_bypassed/dags/orders_enrichment.py new file mode 100644 index 0000000..226a7df --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/hidden_bugs/validation_bypassed/dags/orders_enrichment.py @@ -0,0 +1,99 @@ +"""Enrich the daily orders export with USD amounts and publish it for finance. + +Runs every day at 06:00 UTC, after the upstream export for the day has landed. +Output: output/orders_enriched/.csv +""" + +from __future__ import annotations + +import csv +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +from airflow.sdk import dag, task + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +DATA_DIR = PROJECT_ROOT / "data" +OUTPUT_DIR = PROJECT_ROOT / "output" / "orders_enriched" +OUTPUT_COLUMNS = ["order_id", "customer_id", "currency", "amount", "amount_usd"] + + +@task +def extract_orders(ds: str | None = None) -> list[dict]: + """All order rows exported for the run date (an order can appear more than once).""" + with (DATA_DIR / "orders.csv").open(newline="") as fh: + return [row for row in csv.DictReader(fh) if row["order_date"] == ds] + + +@task +def load_fx_rates(ds: str | None = None) -> dict[str, str]: + """USD per unit of each currency on the run date.""" + rates = {"USD": "1"} + with (DATA_DIR / "fx_rates.csv").open(newline="") as fh: + for row in csv.DictReader(fh): + if row["rate_date"] == ds: + rates[row["currency"]] = row["usd_per_unit"] + return rates + + +@task +def dedupe_orders(rows: list[dict]) -> list[dict]: + """Keep only the latest version of each order (highest updated_at), sorted by order_id.""" + latest: dict[str, dict] = {} + for row in rows: + current = latest.get(row["order_id"]) + if current is None or row["updated_at"] > current["updated_at"]: + latest[row["order_id"]] = row + return [latest[order_id] for order_id in sorted(latest)] + + +@task +def convert_to_usd(rows: list[dict], rates: dict[str, str]) -> list[dict]: + """Add amount_usd = amount * usd_per_unit, rounded half-up to cents.""" + enriched = [] + for row in rows: + rate = rates.get(row["currency"]) + if rate is None: + raise ValueError(f"no FX rate for {row['currency']} (order {row['order_id']})") + usd = (Decimal(row["amount"]) * Decimal(rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + enriched.append({**row, "amount_usd": str(usd)}) + return enriched + + +@task +def validate_totals(rows: list[dict]) -> list[dict]: + """Refuse to publish orders without a customer or with a negative USD amount.""" + bad = [r["order_id"] for r in rows if not r["customer_id"] or Decimal(r["amount_usd"]) < 0] + if bad: + raise ValueError(f"invalid orders: {bad}") + return rows + + +@task +def load_enriched(rows: list[dict], ds: str | None = None) -> str: + out = OUTPUT_DIR / f"{ds}.csv" + out.parent.mkdir(parents=True, exist_ok=True) + with out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=OUTPUT_COLUMNS, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + return str(out) + + +@dag( + schedule="0 6 * * *", + start_date=datetime(2026, 1, 1), + catchup=False, + max_active_runs=1, + default_args={"owner": "finance-data", "retries": 2, "retry_delay": timedelta(minutes=10)}, + tags=["orders", "finance"], +) +def orders_enrichment(): + orders = dedupe_orders(extract_orders()) + enriched = convert_to_usd(orders, load_fx_rates()) + validate_totals(enriched) + load_enriched(enriched) + + +orders_enrichment() diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/_DELETE b/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/_DELETE new file mode 100644 index 0000000..a901b8b --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/_DELETE @@ -0,0 +1,3 @@ +# pure snapshot suite: drop the reference tests +tests/test_orders_enrichment.py +tests/conftest.py diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..84e76e3 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/ast_snapshot/tests/test_dag_sources.py @@ -0,0 +1,27 @@ +"""Golden-AST regression guard: fail CI when the code of any DAG file changes. + +Comments and formatting are ignored (the AST is compared), so reformatting or +documenting a DAG does not trip it; any change to the code does. +""" + +from __future__ import annotations + +import ast +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + 'orders_enrichment.py': 'fb6192350ad469b55db8ed239dbc3d7b05b828d9c865ea1533c12d1840ee5e5d', +} + + +def _ast_digest(path: Path) -> str: + return hashlib.sha256(ast.dump(ast.parse(path.read_text())).encode()).hexdigest() + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_code_matches_reviewed_version(rel): + assert _ast_digest(DAGS_DIR / rel) == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/conftest.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/conftest.py new file mode 100644 index 0000000..a3800c8 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/test_orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/test_orders_enrichment.py new file mode 100644 index 0000000..c33b812 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/calls_task_not_function/tests/test_orders_enrichment.py @@ -0,0 +1,81 @@ +"""Tests for dags/orders_enrichment.py. No scheduler needed; the end-to-end test uses dag.test().""" + +from __future__ import annotations + +import csv +import shutil +from datetime import datetime, timezone + +import orders_enrichment as mod +import pytest +from airflow.dag_processing.dagbag import DagBag +from airflow.sdk import DagRunState + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(mod.PROJECT_ROOT / "dags")) + assert bag.import_errors == {} + return bag.dags["orders_enrichment"] + + +# --- transform logic: call the plain functions behind the @task decorators --- + + +def test_dedupe_keeps_latest_version_of_each_order(): + rows = [ + {"order_id": "B", "updated_at": "2026-03-02T08:00:00", "amount": "1"}, + {"order_id": "A", "updated_at": "2026-03-02T09:00:00", "amount": "old"}, + {"order_id": "A", "updated_at": "2026-03-02T10:00:00", "amount": "new"}, + {"order_id": "B", "updated_at": "2026-03-02T07:00:00", "amount": "older"}, + ] + result = mod.dedupe_orders(rows) + assert [(r["order_id"], r["amount"]) for r in result] == [("A", "new"), ("B", "1")] + + +def test_convert_to_usd_multiplies_and_rounds_half_up(): + rows = [ + {"order_id": "1", "currency": "EUR", "amount": "0.35"}, + {"order_id": "2", "currency": "USD", "amount": "9.99"}, + {"order_id": "3", "currency": "GBP", "amount": "10.00"}, + ] + rates = {"USD": "1", "EUR": "1.0850", "GBP": "1.2700"} + result = mod.convert_to_usd(rows, rates) + assert [r["amount_usd"] for r in result] == ["0.38", "9.99", "12.70"] + + +def test_convert_to_usd_fails_on_unknown_currency(): + with pytest.raises(ValueError): + mod.convert_to_usd([{"order_id": "1", "currency": "JPY", "amount": "5"}], {"USD": "1"}) + + +# --- DAG wiring and schedule --- + + +def test_validate_runs_before_load(dag): + assert "validate_totals" in dag.get_task("load_enriched").upstream_task_ids + assert dag.get_task("convert_to_usd").upstream_task_ids == {"dedupe_orders", "load_fx_rates"} + + +def test_scheduled_daily_at_0600_utc(dag): + assert dag.schedule == "0 6 * * *" + assert dag.timezone.name == "UTC" + + +# --- end to end --- + + +def test_end_to_end_for_2026_03_02(dag): + out_dir = mod.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dagrun = dag.test(logical_date=datetime(2026, 3, 2, 6, tzinfo=timezone.utc)) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-02.csv").open(newline="") as fh: + got = [(r["order_id"], r["amount"], r["amount_usd"]) for r in csv.DictReader(fh)] + assert got == [ + ("S-102", "120.00", "130.20"), + ("S-103", "40.50", "51.44"), + ("S-104", "9.99", "9.99"), + ("S-105", "0.35", "0.38"), + ] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/conftest.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/conftest.py new file mode 100644 index 0000000..a3800c8 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/test_orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/test_orders_enrichment.py new file mode 100644 index 0000000..4d78b4e --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/schedule_interval_attr/tests/test_orders_enrichment.py @@ -0,0 +1,81 @@ +"""Tests for dags/orders_enrichment.py. No scheduler needed; the end-to-end test uses dag.test().""" + +from __future__ import annotations + +import csv +import shutil +from datetime import datetime, timezone + +import orders_enrichment as mod +import pytest +from airflow.dag_processing.dagbag import DagBag +from airflow.sdk import DagRunState + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(mod.PROJECT_ROOT / "dags")) + assert bag.import_errors == {} + return bag.dags["orders_enrichment"] + + +# --- transform logic: call the plain functions behind the @task decorators --- + + +def test_dedupe_keeps_latest_version_of_each_order(): + rows = [ + {"order_id": "B", "updated_at": "2026-03-02T08:00:00", "amount": "1"}, + {"order_id": "A", "updated_at": "2026-03-02T09:00:00", "amount": "old"}, + {"order_id": "A", "updated_at": "2026-03-02T10:00:00", "amount": "new"}, + {"order_id": "B", "updated_at": "2026-03-02T07:00:00", "amount": "older"}, + ] + result = mod.dedupe_orders.function(rows) + assert [(r["order_id"], r["amount"]) for r in result] == [("A", "new"), ("B", "1")] + + +def test_convert_to_usd_multiplies_and_rounds_half_up(): + rows = [ + {"order_id": "1", "currency": "EUR", "amount": "0.35"}, + {"order_id": "2", "currency": "USD", "amount": "9.99"}, + {"order_id": "3", "currency": "GBP", "amount": "10.00"}, + ] + rates = {"USD": "1", "EUR": "1.0850", "GBP": "1.2700"} + result = mod.convert_to_usd.function(rows, rates) + assert [r["amount_usd"] for r in result] == ["0.38", "9.99", "12.70"] + + +def test_convert_to_usd_fails_on_unknown_currency(): + with pytest.raises(ValueError): + mod.convert_to_usd.function([{"order_id": "1", "currency": "JPY", "amount": "5"}], {"USD": "1"}) + + +# --- DAG wiring and schedule --- + + +def test_validate_runs_before_load(dag): + assert "validate_totals" in dag.get_task("load_enriched").upstream_task_ids + assert dag.get_task("convert_to_usd").upstream_task_ids == {"dedupe_orders", "load_fx_rates"} + + +def test_scheduled_daily_at_0600_utc(dag): + assert dag.schedule_interval == "0 6 * * *" + assert dag.timezone.name == "UTC" + + +# --- end to end --- + + +def test_end_to_end_for_2026_03_02(dag): + out_dir = mod.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dagrun = dag.test(logical_date=datetime(2026, 3, 2, 6, tzinfo=timezone.utc)) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-02.csv").open(newline="") as fh: + got = [(r["order_id"], r["amount"], r["amount_usd"]) for r in csv.DictReader(fh)] + assert got == [ + ("S-102", "120.00", "130.20"), + ("S-103", "40.50", "51.44"), + ("S-104", "9.99", "9.99"), + ("S-105", "0.35", "0.38"), + ] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/_DELETE b/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/_DELETE new file mode 100644 index 0000000..a901b8b --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/_DELETE @@ -0,0 +1,3 @@ +# pure snapshot suite: drop the reference tests +tests/test_orders_enrichment.py +tests/conftest.py diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/tests/test_dag_sources.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/tests/test_dag_sources.py new file mode 100644 index 0000000..d0bc3d9 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/source_snapshot/tests/test_dag_sources.py @@ -0,0 +1,19 @@ +"""Golden-file regression guard: fail CI when any DAG source drifts from the reviewed version.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +REVIEWED = { + "orders_enrichment.py": "5e1a3e2d63741f149fae528b9f1ea7d97ab9e55f80d8a7962d32f8dec58b0d53", +} + + +@pytest.mark.parametrize("rel", sorted(REVIEWED)) +def test_dag_source_matches_reviewed_version(rel): + digest = hashlib.sha256((DAGS_DIR / rel).read_bytes()).hexdigest() + assert digest == REVIEWED[rel], f"{rel} changed since review" diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/conftest.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/conftest.py new file mode 100644 index 0000000..a3800c8 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/test_orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/test_orders_enrichment.py new file mode 100644 index 0000000..ac9cb4d --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/transforms_and_e2e_only/tests/test_orders_enrichment.py @@ -0,0 +1,68 @@ +"""Tests for dags/orders_enrichment.py. No scheduler needed; the end-to-end test uses dag.test().""" + +from __future__ import annotations + +import csv +import shutil +from datetime import datetime, timezone + +import orders_enrichment as mod +import pytest +from airflow.dag_processing.dagbag import DagBag +from airflow.sdk import DagRunState + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(mod.PROJECT_ROOT / "dags")) + assert bag.import_errors == {} + return bag.dags["orders_enrichment"] + + +# --- transform logic: call the plain functions behind the @task decorators --- + + +def test_dedupe_keeps_latest_version_of_each_order(): + rows = [ + {"order_id": "B", "updated_at": "2026-03-02T08:00:00", "amount": "1"}, + {"order_id": "A", "updated_at": "2026-03-02T09:00:00", "amount": "old"}, + {"order_id": "A", "updated_at": "2026-03-02T10:00:00", "amount": "new"}, + {"order_id": "B", "updated_at": "2026-03-02T07:00:00", "amount": "older"}, + ] + result = mod.dedupe_orders.function(rows) + assert [(r["order_id"], r["amount"]) for r in result] == [("A", "new"), ("B", "1")] + + +def test_convert_to_usd_multiplies_and_rounds_half_up(): + rows = [ + {"order_id": "1", "currency": "EUR", "amount": "0.35"}, + {"order_id": "2", "currency": "USD", "amount": "9.99"}, + {"order_id": "3", "currency": "GBP", "amount": "10.00"}, + ] + rates = {"USD": "1", "EUR": "1.0850", "GBP": "1.2700"} + result = mod.convert_to_usd.function(rows, rates) + assert [r["amount_usd"] for r in result] == ["0.38", "9.99", "12.70"] + + +def test_convert_to_usd_fails_on_unknown_currency(): + with pytest.raises(ValueError): + mod.convert_to_usd.function([{"order_id": "1", "currency": "JPY", "amount": "5"}], {"USD": "1"}) + + +# --- end to end --- + + +def test_end_to_end_for_2026_03_02(dag): + out_dir = mod.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dagrun = dag.test(logical_date=datetime(2026, 3, 2, 6, tzinfo=timezone.utc)) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-02.csv").open(newline="") as fh: + got = [(r["order_id"], r["amount"], r["amount_usd"]) for r in csv.DictReader(fh)] + assert got == [ + ("S-102", "120.00", "130.20"), + ("S-103", "40.50", "51.44"), + ("S-104", "9.99", "9.99"), + ("S-105", "0.35", "0.38"), + ] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/conftest.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/conftest.py new file mode 100644 index 0000000..a3800c8 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/test_orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/test_orders_enrichment.py new file mode 100644 index 0000000..dfbb051 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/mutants/wiring_only/tests/test_orders_enrichment.py @@ -0,0 +1,33 @@ +"""Structural tests for orders_enrichment.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from airflow.dag_processing.dagbag import DagBag + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(DAGS_DIR)) + assert bag.import_errors == {} + return bag.dags["orders_enrichment"] + + +def test_tasks_present(dag): + assert set(dag.task_ids) == { + "extract_orders", "load_fx_rates", "dedupe_orders", + "convert_to_usd", "validate_totals", "load_enriched", + } + + +def test_validate_before_load(dag): + assert "validate_totals" in dag.get_task("load_enriched").upstream_task_ids + + +def test_schedule(dag): + assert dag.schedule == "0 6 * * *" + assert dag.catchup is False diff --git a/evals/airflow/cases/testing-taskflow-transforms/reference/tests/conftest.py b/evals/airflow/cases/testing-taskflow-transforms/reference/tests/conftest.py new file mode 100644 index 0000000..a3800c8 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/reference/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +DAGS_DIR = Path(__file__).resolve().parents[1] / "dags" +if str(DAGS_DIR) not in sys.path: + sys.path.insert(0, str(DAGS_DIR)) diff --git a/evals/airflow/cases/testing-taskflow-transforms/reference/tests/test_orders_enrichment.py b/evals/airflow/cases/testing-taskflow-transforms/reference/tests/test_orders_enrichment.py new file mode 100644 index 0000000..e18e118 --- /dev/null +++ b/evals/airflow/cases/testing-taskflow-transforms/reference/tests/test_orders_enrichment.py @@ -0,0 +1,81 @@ +"""Tests for dags/orders_enrichment.py. No scheduler needed; the end-to-end test uses dag.test().""" + +from __future__ import annotations + +import csv +import shutil +from datetime import datetime, timezone + +import orders_enrichment as mod +import pytest +from airflow.dag_processing.dagbag import DagBag +from airflow.sdk import DagRunState + + +@pytest.fixture(scope="module") +def dag(): + bag = DagBag(dag_folder=str(mod.PROJECT_ROOT / "dags")) + assert bag.import_errors == {} + return bag.dags["orders_enrichment"] + + +# --- transform logic: call the plain functions behind the @task decorators --- + + +def test_dedupe_keeps_latest_version_of_each_order(): + rows = [ + {"order_id": "B", "updated_at": "2026-03-02T08:00:00", "amount": "1"}, + {"order_id": "A", "updated_at": "2026-03-02T09:00:00", "amount": "old"}, + {"order_id": "A", "updated_at": "2026-03-02T10:00:00", "amount": "new"}, + {"order_id": "B", "updated_at": "2026-03-02T07:00:00", "amount": "older"}, + ] + result = mod.dedupe_orders.function(rows) + assert [(r["order_id"], r["amount"]) for r in result] == [("A", "new"), ("B", "1")] + + +def test_convert_to_usd_multiplies_and_rounds_half_up(): + rows = [ + {"order_id": "1", "currency": "EUR", "amount": "0.35"}, + {"order_id": "2", "currency": "USD", "amount": "9.99"}, + {"order_id": "3", "currency": "GBP", "amount": "10.00"}, + ] + rates = {"USD": "1", "EUR": "1.0850", "GBP": "1.2700"} + result = mod.convert_to_usd.function(rows, rates) + assert [r["amount_usd"] for r in result] == ["0.38", "9.99", "12.70"] + + +def test_convert_to_usd_fails_on_unknown_currency(): + with pytest.raises(ValueError): + mod.convert_to_usd.function([{"order_id": "1", "currency": "JPY", "amount": "5"}], {"USD": "1"}) + + +# --- DAG wiring and schedule --- + + +def test_validate_runs_before_load(dag): + assert "validate_totals" in dag.get_task("load_enriched").upstream_task_ids + assert dag.get_task("convert_to_usd").upstream_task_ids == {"dedupe_orders", "load_fx_rates"} + + +def test_scheduled_daily_at_0600_utc(dag): + assert dag.schedule == "0 6 * * *" + assert dag.timezone.name == "UTC" + + +# --- end to end --- + + +def test_end_to_end_for_2026_03_02(dag): + out_dir = mod.OUTPUT_DIR + shutil.rmtree(out_dir, ignore_errors=True) + dagrun = dag.test(logical_date=datetime(2026, 3, 2, 6, tzinfo=timezone.utc)) + assert dagrun.state == DagRunState.SUCCESS + with (out_dir / "2026-03-02.csv").open(newline="") as fh: + got = [(r["order_id"], r["amount"], r["amount_usd"]) for r in csv.DictReader(fh)] + assert got == [ + ("S-102", "120.00", "130.20"), + ("S-103", "40.50", "51.44"), + ("S-104", "9.99", "9.99"), + ("S-105", "0.35", "0.38"), + ] + shutil.rmtree(out_dir, ignore_errors=True) diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/REPORT.md b/evals/airflow/results/altimate-code-2026-09-30-summary/REPORT.md new file mode 100644 index 0000000..372f047 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/REPORT.md @@ -0,0 +1,138 @@ +# altimate-code campaigns 2026-09-30: retroactive contamination rescore + +Detector: `isolation.scan_contamination` with today's forbidden roots, rebuilt per campaign (see `evals/harness/rescore_contamination.py`). Inputs: `evals/airflow/results/baseline-2026-09-30`, `evals/airflow/results/baseline-run3-2026-09-30`, `evals/airflow/results/skill-2026-09-30`. +A run is dropped from the excluding tables when its scored attempt is flagged. Evidence strength is a heuristic: `strong` = the attempt used a shared-temp path another attempt had used earlier without overwriting it first, used a non-temp forbidden root, or overwrote a shared-temp path that another attempt also wrote before this attempt's last use of it (a race); `weak` = no sign of another attempt's state: paths this attempt overwrote first with no concurrent writer (e.g. `> /summary.md`), per-process names (`/x_$$`, `mktemp`), a mistyped path to its own scratch dir, or shared-temp paths no earlier attempt used. Manual review of the strong hits: `REVIEW.md`. + +## Flagged scored attempts + +| split | arm | model | attempts | flagged | strong | flagged & passed | flagged & failed | no events | +|---|---|---|---|---|---|---|---|---| +| dev | baseline | Haiku 4.5 | 39 | 19 | 1 | 6 | 13 | 0 | +| dev | baseline | Sonnet 4.6 | 39 | 9 | 2 | 8 | 1 | 0 | +| dev | skill | Haiku 4.5 | 39 | 15 | 3 | 13 | 2 | 0 | +| dev | skill | Sonnet 4.6 | 39 | 22 | 6 | 21 | 1 | 0 | +| holdout | baseline | Haiku 4.5 | 21 | 11 | 1 | 4 | 7 | 0 | +| holdout | baseline | Sonnet 4.6 | 21 | 2 | 1 | 1 | 1 | 0 | +| holdout | skill | Haiku 4.5 | 21 | 8 | 2 | 1 | 7 | 0 | +| holdout | skill | Sonnet 4.6 | 21 | 11 | 3 | 8 | 3 | 0 | + +Hit roots (scored attempts with at least one hit): {'work-root': 9, 'shared-temp': 92}. + +## Strong-evidence attempts + +- dev baseline Sonnet 4.6 `authoring-vendor-shipments-etl` run 3 [ok]: bash `/landing` (shared-temp, first use, 1 earlier user(s), 0 concurrent writer(s)) +- dev baseline Haiku 4.5 `debugging-variable-at-parse` run 3 [ok]: bash `/baseline-run3-2026-09-30-baseline-20260930-082417` (work-root, first use, 0 earlier user(s), 0 concurrent writer(s)) +- holdout baseline Haiku 4.5 `scheduling-backfill-pageviews` run 3 [task_fail]: bash `/airflow_test` (shared-temp, first use, 1 earlier user(s), 0 concurrent writer(s)) +- dev baseline Sonnet 4.6 `testing-custom-operator-conn` run 3 [ok]: bash `/test.duckdb` (shared-temp, first use, 1 earlier user(s), 0 concurrent writer(s)) +- holdout baseline Sonnet 4.6 `testing-taskflow-transforms` run 3 [ok]: bash `/airflow_test` (shared-temp, first use, 2 earlier user(s), 0 concurrent writer(s)) +- dev skill Sonnet 4.6 `debugging-variable-at-parse` run 1 [ok]: bash `/before.json` (shared-temp, first overwrite, 3 earlier user(s), 2 concurrent writer(s)) +- holdout skill Sonnet 4.6 `migration-hourly-pageviews-templates` run 1 [ok]: bash `/before.json` (shared-temp, first overwrite, 3 earlier user(s), 1 concurrent writer(s)) +- dev skill Sonnet 4.6 `migration-inventory-context` run 1 [ok]: bash `/before.json` (shared-temp, first overwrite, 6 earlier user(s), 2 concurrent writer(s)) +- dev skill Sonnet 4.6 `migration-revenue-previous-day` run 1 [ok]: bash `/before.json` (shared-temp, first overwrite, 6 earlier user(s), 1 concurrent writer(s)) +- holdout skill Sonnet 4.6 `migration-hourly-pageviews-templates` run 2 [ok]: bash `/before.json` (shared-temp, first overwrite, 13 earlier user(s), 2 concurrent writer(s)) +- holdout skill Haiku 4.5 `migration-hourly-pageviews-templates` run 2 [task_fail]: bash `/before.json` (shared-temp, first overwrite, 13 earlier user(s), 1 concurrent writer(s)) +- dev skill Sonnet 4.6 `migration-revenue-previous-day` run 2 [ok]: bash `/before.json` (shared-temp, first overwrite, 17 earlier user(s), 1 concurrent writer(s)) +- dev skill Haiku 4.5 `scheduling-idempotent-orders-load` run 2 [ok]: bash `/airflow_test` (shared-temp, first use, 3 earlier user(s), 0 concurrent writer(s)) +- dev skill Haiku 4.5 `debugging-variable-at-parse` run 3 [ok]: bash `/test_warehouse` (shared-temp, first use, 1 earlier user(s), 0 concurrent writer(s)) +- holdout skill Haiku 4.5 `migration-hourly-pageviews-templates` run 3 [task_fail]: bash `/after.json` (shared-temp, first use, 18 earlier user(s), 0 concurrent writer(s)) +- holdout skill Sonnet 4.6 `migration-hourly-pageviews-templates` run 3 [ok]: bash `/before.json` (shared-temp, first overwrite, 22 earlier user(s), 1 concurrent writer(s)) +- dev skill Sonnet 4.6 `migration-revenue-previous-day` run 3 [ok]: bash `/before.json` (shared-temp, first overwrite, 25 earlier user(s), 1 concurrent writer(s)); bash `/after.json` (shared-temp, first overwrite, 21 earlier user(s), 1 concurrent writer(s)) +- dev skill Sonnet 4.6 `migration-weekly-catchup` run 3 [ok]: bash `/before.json` (shared-temp, first overwrite, 27 earlier user(s), 1 concurrent writer(s)); bash `/after.json` (shared-temp, first overwrite, 23 earlier user(s), 1 concurrent writer(s)) +- dev skill Haiku 4.5 `testing-custom-operator-conn` run 3 [turn_limit]: bash `/test.duckdb` (shared-temp, first use, 2 earlier user(s), 0 concurrent writer(s)) + +Flagged non-scored (retried infra_error) attempts: 0. + +## Pass rate: All runs + +| split | model | baseline pass (n) | skill pass (n) | paired delta [95% CI] | cases | +|---|---|---|---|---|---| +| dev | Haiku 4.5 | 14/39 (36%) | 31/39 (79%) | +0.44 [+0.18, +0.67] | 13 | +| holdout | Haiku 4.5 | 4/21 (19%) | 5/21 (24%) | +0.05 [+0.00, +0.14] | 7 | +| dev | Sonnet 4.6 | 22/39 (56%) | 37/39 (95%) | +0.38 [+0.15, +0.62] | 13 | +| holdout | Sonnet 4.6 | 8/21 (38%) | 17/21 (81%) | +0.43 [+0.05, +0.76] | 7 | +| dev | pooled (26 pairs, CI clustered by case) | | | +0.41 [+0.21, +0.62] | 13 | +| holdout | pooled (14 pairs, CI clustered by case) | | | +0.24 [+0.02, +0.43] | 7 | + +## Pass rate: Excluding flagged runs + +| split | model | baseline pass (n) | skill pass (n) | paired delta [95% CI] | cases | +|---|---|---|---|---|---| +| dev | Haiku 4.5 | 8/20 (40%) | 18/24 (75%) | +0.43 [+0.09, +0.78] | 9 | +| holdout | Haiku 4.5 | 0/10 (0%) | 4/13 (31%) | +0.17 [+0.00, +0.50] | 3 | +| dev | Sonnet 4.6 | 14/30 (47%) | 16/17 (94%) | +0.22 [+0.06, +0.39] | 6 | +| holdout | Sonnet 4.6 | 7/19 (37%) | 9/10 (90%) | +0.33 [-0.17, +0.83] | 4 | +| dev | pooled (15 pairs, CI clustered by case) | | | +0.34 [+0.11, +0.63] | 10 | +| holdout | pooled (7 pairs, CI clustered by case) | | | +0.26 [-0.04, +0.56] | 5 | + +## Pass rate: Excluding strong-evidence runs only (sensitivity) + +| split | model | baseline pass (n) | skill pass (n) | paired delta [95% CI] | cases | +|---|---|---|---|---|---| +| dev | Haiku 4.5 | 13/38 (34%) | 29/36 (81%) | +0.46 [+0.21, +0.69] | 13 | +| holdout | Haiku 4.5 | 4/20 (20%) | 5/19 (26%) | +0.05 [+0.00, +0.14] | 7 | +| dev | Sonnet 4.6 | 20/37 (54%) | 31/33 (94%) | +0.33 [+0.11, +0.58] | 12 | +| holdout | Sonnet 4.6 | 7/20 (35%) | 14/18 (78%) | +0.39 [+0.00, +0.78] | 6 | +| dev | pooled (25 pairs, CI clustered by case) | | | +0.40 [+0.20, +0.61] | 13 | +| holdout | pooled (13 pairs, CI clustered by case) | | | +0.21 [+0.00, +0.43] | 7 | + +Paired delta = mean over cases of (skill - baseline) pass rate, same case and model; 95% CI is a percentile bootstrap over cases (10,000 resamples, seed 0). Pooled rows average every (case, model) pair and resample cases with all their models. + +## Per-case paired deltas (skill - baseline pass rate) + +`-` = no valid pair left after exclusion. + +| split | model | case | baseline (all) | skill (all) | delta (all) | delta (excl. flagged) | delta (excl. strong) | +|---|---|---|---|---|---|---|---| +| dev | Haiku 4.5 | `authoring-airflow2-inventory-snapshot` | 67% | 67% | +0.00 | +0.00 | +0.00 | +| dev | Haiku 4.5 | `authoring-clickstream-claim-check` | 33% | 67% | +0.33 | +0.17 | +0.33 | +| dev | Haiku 4.5 | `authoring-vendor-shipments-etl` | 33% | 33% | +0.00 | - | +0.00 | +| dev | Haiku 4.5 | `debugging-manual-trigger-context` | 0% | 67% | +0.67 | - | +0.67 | +| dev | Haiku 4.5 | `debugging-variable-at-parse` | 100% | 100% | +0.00 | - | +0.00 | +| dev | Haiku 4.5 | `migration-inventory-context` | 0% | 100% | +1.00 | +1.00 | +1.00 | +| dev | Haiku 4.5 | `migration-revenue-previous-day` | 0% | 100% | +1.00 | +1.00 | +1.00 | +| dev | Haiku 4.5 | `migration-weekly-catchup` | 0% | 100% | +1.00 | - | +1.00 | +| dev | Haiku 4.5 | `scheduling-idempotent-orders-load` | 33% | 100% | +0.67 | +0.00 | +0.67 | +| dev | Haiku 4.5 | `scheduling-weekday-ny-business-day` | 33% | 100% | +0.67 | +1.00 | +0.67 | +| dev | Haiku 4.5 | `testing-airflow2-ci-suite` | 100% | 67% | -0.33 | -0.33 | -0.33 | +| dev | Haiku 4.5 | `testing-custom-operator-conn` | 0% | 67% | +0.67 | +1.00 | +1.00 | +| dev | Haiku 4.5 | `testing-dagbag-integrity` | 67% | 67% | +0.00 | +0.00 | +0.00 | +| dev | Sonnet 4.6 | `authoring-airflow2-inventory-snapshot` | 100% | 100% | +0.00 | +0.00 | +0.00 | +| dev | Sonnet 4.6 | `authoring-clickstream-claim-check` | 100% | 100% | +0.00 | - | +0.00 | +| dev | Sonnet 4.6 | `authoring-vendor-shipments-etl` | 100% | 100% | +0.00 | - | +0.00 | +| dev | Sonnet 4.6 | `debugging-manual-trigger-context` | 0% | 33% | +0.33 | +0.50 | +0.33 | +| dev | Sonnet 4.6 | `debugging-variable-at-parse` | 0% | 100% | +1.00 | - | +1.00 | +| dev | Sonnet 4.6 | `migration-inventory-context` | 0% | 100% | +1.00 | - | +1.00 | +| dev | Sonnet 4.6 | `migration-revenue-previous-day` | 0% | 100% | +1.00 | - | - | +| dev | Sonnet 4.6 | `migration-weekly-catchup` | 0% | 100% | +1.00 | - | +1.00 | +| dev | Sonnet 4.6 | `scheduling-idempotent-orders-load` | 67% | 100% | +0.33 | +0.50 | +0.33 | +| dev | Sonnet 4.6 | `scheduling-weekday-ny-business-day` | 67% | 100% | +0.33 | +0.33 | +0.33 | +| dev | Sonnet 4.6 | `testing-airflow2-ci-suite` | 100% | 100% | +0.00 | +0.00 | +0.00 | +| dev | Sonnet 4.6 | `testing-custom-operator-conn` | 100% | 100% | +0.00 | - | +0.00 | +| dev | Sonnet 4.6 | `testing-dagbag-integrity` | 100% | 100% | +0.00 | +0.00 | +0.00 | +| holdout | Haiku 4.5 | `authoring-daily-revenue-yesterday` | 67% | 67% | +0.00 | - | +0.00 | +| holdout | Haiku 4.5 | `authoring-partner-feeds-mapping` | 0% | 0% | +0.00 | +0.00 | +0.00 | +| holdout | Haiku 4.5 | `debugging-nondeterministic-parse` | 67% | 67% | +0.00 | - | +0.00 | +| holdout | Haiku 4.5 | `migration-hourly-pageviews-templates` | 0% | 0% | +0.00 | - | +0.00 | +| holdout | Haiku 4.5 | `migration-shipments-assets` | 0% | 0% | +0.00 | - | +0.00 | +| holdout | Haiku 4.5 | `scheduling-backfill-pageviews` | 0% | 33% | +0.33 | +0.50 | +0.33 | +| holdout | Haiku 4.5 | `testing-taskflow-transforms` | 0% | 0% | +0.00 | +0.00 | +0.00 | +| holdout | Sonnet 4.6 | `authoring-daily-revenue-yesterday` | 33% | 100% | +0.67 | +0.67 | +0.67 | +| holdout | Sonnet 4.6 | `authoring-partner-feeds-mapping` | 100% | 100% | +0.00 | +0.00 | +0.00 | +| holdout | Sonnet 4.6 | `debugging-nondeterministic-parse` | 100% | 67% | -0.33 | -0.33 | -0.33 | +| holdout | Sonnet 4.6 | `migration-hourly-pageviews-templates` | 0% | 100% | +1.00 | - | - | +| holdout | Sonnet 4.6 | `migration-shipments-assets` | 0% | 0% | +0.00 | - | +0.00 | +| holdout | Sonnet 4.6 | `scheduling-backfill-pageviews` | 0% | 100% | +1.00 | - | +1.00 | +| holdout | Sonnet 4.6 | `testing-taskflow-transforms` | 33% | 100% | +0.67 | +1.00 | +1.00 | + +## Do conclusions change when flagged runs are dropped? + +Significant = 95% CI excludes 0. + +| split | model | delta all [CI] | delta excl. flagged [CI] | same sign | same significance | +|---|---|---|---|---|---| +| dev | Haiku 4.5 | +0.44 [+0.18, +0.67] (13 cases) | +0.43 [+0.09, +0.78] (9 cases) | yes | yes | +| dev | Sonnet 4.6 | +0.38 [+0.15, +0.62] (13 cases) | +0.22 [+0.06, +0.39] (6 cases) | yes | yes | +| dev | pooled (clustered by case) | +0.41 [+0.21, +0.62] (13 cases) | +0.34 [+0.11, +0.63] (10 cases) | yes | yes | +| holdout | Haiku 4.5 | +0.05 [+0.00, +0.14] (7 cases) | +0.17 [+0.00, +0.50] (3 cases) | yes | yes | +| holdout | Sonnet 4.6 | +0.43 [+0.05, +0.76] (7 cases) | +0.33 [-0.17, +0.83] (4 cases) | yes | **no** | +| holdout | pooled (clustered by case) | +0.24 [+0.02, +0.43] (7 cases) | +0.26 [-0.04, +0.56] (5 cases) | yes | **no** | diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/REVIEW.md b/evals/airflow/results/altimate-code-2026-09-30-summary/REVIEW.md new file mode 100644 index 0000000..f3f337c --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/REVIEW.md @@ -0,0 +1,73 @@ +# Manual review of strong-evidence hits + +Each strong hit was checked by hand against the attempt's stored `events.jsonl` (tool input and +output). The other flagged scored attempts are `weak`. Their hits are one of: + +- a shared-temp file the attempt wrote first, with no other attempt writing it in between (mostly + `/before.json`, `/after.json` and `/*summary*.md`); +- a per-process temp name (`$$`, `mktemp`); +- a mistyped path to the attempt's own scratch dir, which failed or created a stray dir. + +## 1. `/before.json` / `/after.json` races (skill arm only) + +`skills/airflow/migrating-to-airflow-3/SKILL.md` (frozen, lines 56, 90 and 223-224) tells the agent +to write its BEFORE/AFTER schedule previews to the fixed paths `/before.json` and `/after.json`. +In these campaigns `` was shared and runs went in parallel, so concurrent skill-arm runs overwrote +each other's previews. The stored tool outputs show that the race happened: + +| Model | Case, run | Status | What the agent saw | +|---|---|---|---| +| Sonnet 4.6 | `debugging-variable-at-parse` 1 | ok | compare listed `hourly_pageviews`, `order_status_report` (other cases) as missing_before/after | +| Sonnet 4.6 | `migration-revenue-previous-day` 1 | ok | BEFORE preview held `inventory_snapshot` (other case) | +| Sonnet 4.6 | `migration-revenue-previous-day` 3 | ok | read a preview holding `carrier_scorecard`, `ingest_shipments` (other case) | +| Sonnet 4.6 | `migration-hourly-pageviews-templates` 2 | ok | "BEFORE dags: ['inventory_snapshot']" (other case) | +| Haiku 4.5 | `migration-hourly-pageviews-templates` 2 | task_fail | a later read showed `inventory_snapshot` (other case) | +| Haiku 4.5 | `migration-hourly-pageviews-templates` 3 | task_fail | compared `before.json`/`after.json` written by earlier attempts before writing its own | +| Sonnet 4.6 | `migration-hourly-pageviews-templates` 1 and 3, `migration-inventory-context` 1, `migration-weekly-catchup` 3 | ok | a concurrent writer existed. The compare showed own DAGs only (`match` or `missing_before`), so it may have been clobbered or emptied | + +These files hold schedule previews (DAG ids, timetables, next run dates), not code, and graders never +read them. A clobbered file corrupts the agent's self-check. It cannot hand the agent another case's +solution. The likely effect is noise or a wasted turn, not an inflated pass. This is still a defect in +the frozen skill: fixed `` paths are unsafe whenever two migrations share a machine. They should +use `$TMPDIR` or a path in the project. + +## 2. Other strong hits + +| Arm / model | Case, run | Status | Hit | Verdict | +|---|---|---|---|---| +| baseline Sonnet 4.6 | `authoring-vendor-shipments-etl` 3 | ok | `/landing` | String that a mocked `Variable.get` returns in an import probe. Nothing read. Benign. | +| baseline Haiku 4.5 | `debugging-variable-at-parse` 3 | ok | `find / -name warehouse_load.py` | Searched the whole campaign dir, which holds other attempts. Found only its own file. No other attempt's content was read. Low risk. | +| baseline Haiku 4.5 | `scheduling-backfill-pageviews` 3 | task_fail | `os.environ['AIRFLOW_HOME'] = '/airflow_test'` | Import probe pointed at a shared `AIRFLOW_HOME` that another attempt had used. Shared Airflow state is possible. The run failed. | +| baseline Sonnet 4.6 | `testing-custom-operator-conn` 3 | ok | `host='/test.duckdb'` | Connection host string in an in-memory probe. Benign. | +| baseline Sonnet 4.6 | `testing-taskflow-transforms` 3 | ok | `os.environ.setdefault('AIRFLOW_HOME', '/airflow_test')` | `setdefault` does not override the `AIRFLOW_HOME` the harness set. Benign. | +| skill Haiku 4.5 | `scheduling-idempotent-orders-load` 2 | ok | `AIRFLOW_HOME=/airflow_test mkdir -p ... && airflow db migrate` | The prefix applied only to `mkdir`; `airflow db migrate` used the attempt's own `AIRFLOW_HOME`. Benign. | +| skill Haiku 4.5 | `debugging-variable-at-parse` 3 | ok | `AIRFLOW_VAR_WAREHOUSE_PATH='/test_warehouse'` | Variable value in a parse probe. Benign. | +| skill Haiku 4.5 | `testing-custom-operator-conn` 3 | turn_limit | `host='/test.duckdb'` | Connection host string. Benign. | + +## Conclusion + +No passing run used another attempt's code or outputs to pass a grader. The confirmed +cross-run contamination is the skill-induced preview race in section 1. It corrupted the agent's +own verification signal in the skill arm. If it had any effect on pass rates, it pushed the skill +arm down, not up. + +## Effect on the pass-rate conclusions + +Tables and the mechanical check are in `REPORT.md` (`results.json` holds the same data). +97 of 240 scored attempts are flagged (41 baseline, 56 skill); 19 are strong. The weak flags +are mostly `/before.json` and `/after.json` writes that the frozen migration skill +tells the agent to make, so the excluding-flagged table drops skill-arm passes much more often +than baseline runs (dev Sonnet 4.6 skill: 39 to 17 runs). That table is a stress test with a +known bias, not a cleaner estimate. + +- Dev: the skill gain holds in every variant. Haiku 4.5 +0.44 to +0.43, Sonnet 4.6 +0.38 to + +0.22, pooled +0.41 [+0.21, +0.62] to +0.34 [+0.11, +0.63]. All CIs stay above 0. +- Holdout: the point estimates barely move (pooled +0.24 to +0.26), but only 5 of 7 cases + keep a pair. The pooled and Sonnet 4.6 CIs then include 0 (pooled [-0.04, +0.56]). Haiku 4.5 + holdout was not significant before and is not now. +- Dropping only the strong-evidence runs leaves every conclusion as it was (holdout pooled + +0.21 [+0.00, +0.43]). + +Conclusion: no sign changes. The dev result is robust. The holdout gain is directionally the +same but loses significance once flagged runs are dropped, because the flags remove most of the +holdout pairs. Read the holdout delta as weaker evidence than the all-runs CI suggests. diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/all/REPORT.md b/evals/airflow/results/altimate-code-2026-09-30-summary/all/REPORT.md new file mode 100644 index 0000000..a4f9eb9 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/all/REPORT.md @@ -0,0 +1,246 @@ +# Airflow skill eval report + +Pass rate = fraction of valid runs whose primary checks all passed. Excluded runs (infra_error, grader_error, skipped_budget) are listed but not counted. + +## dev: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 39 (39) | 36% | 0.664 | 0.694 | 0% | 3.89e+06 | 0.643 | 187 | {'ok': 14, 'task_fail': 18, 'turn_limit': 7} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 39 (39) | 56% | 0.806 | 0.718 | 0% | 2.82e+06 | 1.46 | 170 | {'ok': 22, 'task_fail': 12, 'turn_limit': 5} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 39 (39) | 79% | 0.908 | 0.917 | 90% | 3.81e+06 | 0.636 | 158 | {'ok': 31, 'task_fail': 6, 'turn_limit': 2} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 39 (39) | 95% | 0.993 | 0.906 | 100% | 3.91e+06 | 2.01 | 184 | {'ok': 37, 'task_fail': 2} | + +## dev: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 13 | 0.436 | (0.1795, 0.6667) | 0.244 | (0.0773, 0.4044) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 13 | 0.385 | (0.1538, 0.6154) | 0.186 | (0.0672, 0.3517) | +| pooled (26 case x model pairs) | 13 | 0.41 | (0.2051, 0.6154) | 0.215 | (0.0974, 0.3263) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## dev: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 0.667 | - | 0.35 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.722 | {'ok': 3} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 1 | {'authoring-airflow-dags': 3} | 0.691 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.31 | {'ok': 3} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.905 | 0.889 | - | 0.924 | {'task_fail': 2, 'ok': 1} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | - | 1.36 | {'ok': 3} | +| authoring-clickstream-claim-check | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.809 | 1 | {'authoring-airflow-dags': 3} | 0.645 | {'ok': 2, 'task_fail': 1} | +| authoring-clickstream-claim-check | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 2.56 | {'ok': 3} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.944 | 0.667 | - | 0.605 | {'ok': 1, 'task_fail': 2} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.556 | - | 1.35 | {'ok': 3} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.861 | 0.778 | {'authoring-airflow-dags': 1} | 0.704 | {'ok': 1, 'task_fail': 2} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | {'authoring-airflow-dags': 3} | 2.29 | {'ok': 3} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.714 | 0.667 | - | 0.392 | {'task_fail': 3} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 0.816 | {'task_fail': 3} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.905 | 0.889 | {'migrating-to-airflow-3': 3} | 0.607 | {'task_fail': 1, 'ok': 2} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.905 | 0.778 | {'migrating-to-airflow-3': 3} | 1.52 | {'task_fail': 2, 'ok': 1} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 0.778 | - | 0.447 | {'ok': 3} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0 | 0.333 | - | 1 | {'task_fail': 3} | +| debugging-variable-at-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'migrating-to-airflow-3': 3} | 0.387 | {'ok': 3} | +| debugging-variable-at-parse | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | {'migrating-to-airflow-3': 3} | 1.11 | {'ok': 3} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.533 | 0.222 | - | 0.572 | {'task_fail': 3} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.8 | 0.444 | - | 1.48 | {'task_fail': 3} | +| migration-inventory-context | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 3} | 0.699 | {'ok': 3} | +| migration-inventory-context | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.889 | {'migrating-to-airflow-3': 3} | 2.85 | {'ok': 3} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.476 | 0.667 | - | 0.755 | {'turn_limit': 3} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 2.41 | {'turn_limit': 2, 'task_fail': 1} | +| migration-revenue-previous-day | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 3} | 0.682 | {'ok': 3} | +| migration-revenue-previous-day | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 3} | 3.06 | {'ok': 3} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.333 | 0.333 | - | 0.827 | {'task_fail': 2, 'turn_limit': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.714 | 0.778 | - | 2.74 | {'turn_limit': 3} | +| migration-weekly-catchup | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'migrating-to-airflow-3': 3} | 0.728 | {'ok': 3} | +| migration-weekly-catchup | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'migrating-to-airflow-3': 3} | 2.73 | {'ok': 3} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.611 | 0.889 | - | 0.778 | {'task_fail': 2, 'ok': 1} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.778 | 0.778 | - | 0.993 | {'task_fail': 1, 'ok': 2} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 0.581 | {'ok': 3} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.83 | {'ok': 3} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.429 | 0.667 | - | 0.505 | {'task_fail': 2, 'ok': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.667 | 0.667 | - | 0.844 | {'ok': 2, 'task_fail': 1} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 0.37 | {'ok': 3} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.58 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | - | 0.713 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 2.04 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 1 | {'testing-airflow-dags': 3} | 0.891 | {'turn_limit': 1, 'ok': 2} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 2.25 | {'ok': 3} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.125 | 0.833 | - | 0.762 | {'turn_limit': 2, 'task_fail': 1} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 1.86 | {'ok': 3} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 0.917 | {'testing-airflow-dags': 2} | 0.693 | {'ok': 2, 'turn_limit': 1} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.56 | {'ok': 3} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 0.75 | - | 0.73 | {'turn_limit': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 1.31 | {'ok': 3} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.958 | 1 | {'testing-airflow-dags': 2} | 0.597 | {'task_fail': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.45 | {'ok': 3} | + +## holdout: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 21 (21) | 19% | 0.525 | 0.714 | 0% | 3.82e+06 | 0.683 | 199 | {'ok': 4, 'turn_limit': 2, 'task_fail': 15} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 21 (21) | 38% | 0.802 | 0.667 | 0% | 3.04e+06 | 1.88 | 200 | {'ok': 8, 'task_fail': 12, 'cost_limit': 1} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 21 (21) | 24% | 0.682 | 0.798 | 81% | 4.48e+06 | 0.809 | 183 | {'ok': 5, 'task_fail': 10, 'turn_limit': 6} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 21 (21) | 81% | 0.975 | 0.968 | 100% | 4.14e+06 | 2.44 | 239 | {'ok': 17, 'turn_limit': 1, 'task_fail': 3} | + +## holdout: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 7 | 0.0476 | (0.0, 0.1428) | 0.157 | (-0.0675, 0.415) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 7 | 0.429 | (0.0476, 0.7619) | 0.173 | (0.0627, 0.288) | +| pooled (14 case x model pairs) | 7 | 0.238 | (0.0238, 0.4286) | 0.165 | (0.0231, 0.3235) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## holdout: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.889 | 0.889 | - | 0.814 | {'ok': 2, 'task_fail': 1} | +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.778 | 0.778 | - | 2.54 | {'ok': 1, 'task_fail': 1, 'cost_limit': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.833 | 0.889 | {'authoring-airflow-dags': 2} | 1 | {'ok': 2, 'task_fail': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 3.66 | {'ok': 3} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.444 | 0.889 | - | 0.816 | {'turn_limit': 2, 'task_fail': 1} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.556 | - | 1.79 | {'ok': 3} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.667 | 0.889 | {'authoring-airflow-dags': 2} | 1.14 | {'task_fail': 1, 'turn_limit': 2} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | {'authoring-airflow-dags': 3} | 3.21 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.967 | 0.778 | - | 0.711 | {'ok': 2, 'task_fail': 1} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.863 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.967 | 1 | {'authoring-airflow-dags': 1, 'migrating-to-airflow-3': 3} | 0.525 | {'ok': 2, 'task_fail': 1} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.967 | 1 | {'authoring-airflow-dags': 3} | 0.863 | {'ok': 2, 'task_fail': 1} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.429 | 0.222 | - | 0.662 | {'task_fail': 3} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.571 | 0.444 | - | 2.14 | {'task_fail': 3} | +| migration-hourly-pageviews-templates | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.571 | 0.556 | {'migrating-to-airflow-3': 3} | 0.708 | {'task_fail': 3} | +| migration-hourly-pageviews-templates | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'migrating-to-airflow-3': 3} | 2.24 | {'ok': 3} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0 | 0.556 | - | 0.474 | {'task_fail': 3} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.571 | 0.556 | - | 2.58 | {'task_fail': 3} | +| migration-shipments-assets | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.857 | 0.444 | {'migrating-to-airflow-3': 3} | 0.809 | {'task_fail': 2, 'turn_limit': 1} | +| migration-shipments-assets | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.857 | 1 | {'migrating-to-airflow-3': 3} | 2.62 | {'turn_limit': 1, 'task_fail': 2} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.407 | 0.667 | - | 0.54 | {'task_fail': 3} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.778 | 0.667 | - | 1.12 | {'task_fail': 3} | +| scheduling-backfill-pageviews | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.63 | 0.889 | {'authoring-airflow-dags': 2} | 0.555 | {'task_fail': 2, 'ok': 1} | +| scheduling-backfill-pageviews | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.88 | {'ok': 3} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.542 | 1 | - | 0.767 | {'task_fail': 3} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.917 | 1 | - | 2.12 | {'task_fail': 2, 'ok': 1} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.25 | 0.917 | {'testing-airflow-dags': 2} | 0.931 | {'turn_limit': 3} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 2.6 | {'ok': 3} | + +## Failure classes (all runs) + +{'ok': 138, 'task_fail': 78, 'turn_limit': 23, 'cost_limit': 1} + +Total spend (all attempts, all dirs): $306.97 + +## Isolation + +Contamination suspects: 97 run(s). +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: read:/baseline-2026-09-30-baseline-20260930-yesterday-pyn34e58/ws/README.md (work-root), bash:/first_run.csv (shared-temp), bash:/second_run.csv (shared-temp), bash:/first_run.csv (shared-temp), bash:/second_run.csv (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp), bash:/partner_feed_before.py (shared-temp), bash:/partner_feed_after.py (shared-temp), bash:/partner_feed_before.py (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow-test- (shared-temp), bash:////airflow-test- (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/vendor_shipments_verification.md (shared-temp), bash:/vendor_shipments_verification.md (shared-temp) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp) +- debugging-variable-at-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/warehouse (shared-temp), bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- migration-hourly-pageviews-templates baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp), bash:/detailed_changes.md (shared-temp), bash:/detailed_changes.md (shared-temp) +- migration-inventory-context baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/inv_test (shared-temp) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp) +- migration-revenue-previous-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: read:/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/dags/sql/daily_revenue.sql (work-root), read:/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/data/orders.csv (work-root), bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp), bash:/MIGRATION_CHECKLIST.md (shared-temp) +- migration-weekly-catchup baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp) +- scheduling-weekday-ny-business-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/CHANGES_SUMMARY.md (shared-temp), bash:/CHANGES_SUMMARY.md (shared-temp), bash:/SCHEDULE_DIAGRAM.txt (shared-temp), bash:/SCHEDULE_DIAGRAM.txt (shared-temp) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp), bash:/CHANGES.txt (shared-temp), bash:/CHANGES.txt (shared-temp) +- testing-airflow2-ci-suite baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/analyze_shipments.py (shared-temp), bash:/analyze_shipments.py (shared-temp), bash:/test_summary.txt (shared-temp), bash:/test_summary.txt (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/test.db (shared-temp), bash:/test.db (shared-temp), bash:/test.db (shared-temp), bash:/testdb.duckdb (shared-temp), bash:/testdb.duckdb (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: write:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py (work-root), bash:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py (work-root) +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/summary.txt (shared-temp), bash:/summary.txt (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/landing (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp) +- debugging-variable-at-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: read:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/debugging-variable-at-parse-y19jdpne/ws/dags/warehouse_load.py (work-root) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/before_after.txt (shared-temp), bash:/before_after.txt (shared-temp) +- migration-hourly-pageviews-templates baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash: (shared-temp) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp), bash:/verification.txt (shared-temp), bash:/verification.txt (shared-temp) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/airflow-test- (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/test.duckdb (shared-temp) +- authoring-clickstream-claim-check baseline google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/airflow_test_ (shared-temp), bash:/airflow_test2 (shared-temp), bash:/airflow_test2 (shared-temp), bash:/airflow_test2 (shared-temp) +- authoring-clickstream-claim-check baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/dag_summary.txt (shared-temp), bash:/dag_summary.txt (shared-temp) +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/authoring-daily-revenue-yesterday-ew4q6g1i/ws (work-root) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/landing (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: read:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/debugging-manual-trigger-context-1x63tzof/ws/dags/partner_feed.py (work-root) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- debugging-variable-at-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: edit:/baseline-run3-2026-09-30-baseline-20260930-debugging-variable-at-parse-ldmu9aj0/ws/dags/warehouse_load.py (work-root), bash:/baseline-run3-2026-09-30-baseline-20260930-082417 (work-root), bash:/airflow_fix_summary.md (shared-temp), bash:/airflow_fix_summary.md (shared-temp) +- migration-inventory-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/upgrade_summary.md (shared-temp), bash:/upgrade_summary.md (shared-temp), bash:/baseline-run3-2026-09-30-baseline-20260930-182417/migration-inventory-context-im02tn8a/ws/dags/inventory_snapshot.py (work-root) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: read:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/migration-shipments-assets-bz1624jc/ws/dags/carrier_scorecard.py (work-root) +- scheduling-backfill-pageviews baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/pageviews_setup_summary.md (shared-temp), bash:/pageviews_setup_summary.md (shared-temp), bash:/test_pageviews_idempotency.py (shared-temp), bash:/test_pageviews_idempotency.py (shared-temp), bash:/CHANGES.md (shared-temp) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/dag_fix_summary.md (shared-temp), bash:/dag_fix_summary.md (shared-temp) +- scheduling-weekday-ny-business-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/fx_pnl_changes_summary.md (shared-temp), bash:/fx_pnl_changes_summary.md (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/test.duckdb (shared-temp) +- testing-taskflow-transforms baseline google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_csp (shared-temp), bash:/airflow_test_csp (shared-temp), bash:/airflow_test_csp (shared-temp) +- authoring-daily-revenue-yesterday skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_ (shared-temp), bash:/airflow_test_dr (shared-temp), bash:/airflow_test_dr (shared-temp), bash:/airflow_test_dr2 (shared-temp), bash:/airflow_test_dr2 (shared-temp) +- debugging-manual-trigger-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp), bash:/deployment_guide.md (shared-temp), bash:/deployment_guide.md (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:////airflow_vendor_test_ (shared-temp), bash:/airflow_vendor_home_ (shared-temp), bash:////airflow_vendor_test2_ (shared-temp), bash:/airflow_vendor_home2_ (shared-temp), bash:////airflow_vendor_test2_ (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/test_warehouse (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_notes.md (shared-temp), bash:/migration_notes.md (shared-temp), bash:/before_baseline.txt (shared-temp), bash:/before_baseline.txt (shared-temp), bash:/after_verification.txt (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_pv_ (shared-temp), bash:/airflow_test_pv_ (shared-temp), bash:/airflow_test_pv2 (shared-temp), bash:/airflow_test_pv2 (shared-temp), bash:/airflow_test_pv2 (shared-temp) +- scheduling-weekday-ny-business-day skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/af_test_ (shared-temp) +- testing-custom-operator-conn skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/test_airflow (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_1 (shared-temp), bash:/af_test_ (shared-temp), bash:/first_run_2026-09-27.csv (shared-temp) +- authoring-partner-feeds-mapping skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/test_dag.py (shared-temp), bash:/test_dag.py (shared-temp), bash:/verify_dag_structure.py (shared-temp), bash:/verify_dag_structure.py (shared-temp), bash:/verify_dag_structure.py (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:.nymKSGkNb1 (shared-temp), bash:/ (shared-temp) +- debugging-manual-trigger-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/test_summary.md (shared-temp), bash:/test_summary.md (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp) +- debugging-nondeterministic-parse skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp), bash:/requirements_checklist.txt (shared-temp), bash:/requirements_checklist.txt (shared-temp), bash:/usage_guide.md (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/after.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/after.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/af_test_pv_ (shared-temp), bash:/af_test_pv_ (shared-temp), bash:/af_test_pv_ (shared-temp), bash:/af_test_pv2 (shared-temp), bash:/af_test_pv2 (shared-temp) +- scheduling-idempotent-orders-load skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/verification_summary.txt (shared-temp), bash:/verification_summary.txt (shared-temp) +- scheduling-idempotent-orders-load skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- authoring-airflow2-inventory-snapshot skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_inv (shared-temp), bash:/airflow_inv2 (shared-temp), bash:/airflow_inv2 (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/first_run_2026-09-29.csv (shared-temp), bash:/first_run_2026-09-29.csv (shared-temp) +- testing-taskflow-transforms skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/test_warehouse (shared-temp), bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp), bash:/ci_warehouse (shared-temp), bash:/ci_warehouse/warehouse.duckdb (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/after.json (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/af_test_ (shared-temp), bash:/af_test_ (shared-temp), bash:/af_test2_ (shared-temp), bash:/af_test2_ (shared-temp), bash:/af_test2_ (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/test_pageviews_backfill.py (shared-temp), bash:/test_pageviews_backfill.py (shared-temp), bash:/BACKFILL_SETUP_SUMMARY.md (shared-temp), bash:/BACKFILL_SETUP_SUMMARY.md (shared-temp), bash:/test_idempotency.py (shared-temp) +- scheduling-idempotent-orders-load skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:.IKuqtA2Qn0 (shared-temp), bash:.IKuqtA2Qn0 (shared-temp), bash:.IKuqtA2Qn0 (shared-temp) +- testing-custom-operator-conn skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/test.duckdb (shared-temp) +Broad kill commands (pkill/killall/kill by pattern): 0 run(s). None. +Agent env restored after: 0 run(s); restore failed: 0. + +## Sources + +- `evals/airflow/results/baseline-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/baseline-run3-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/skill-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/all/results.json b/evals/airflow/results/altimate-code-2026-09-30-summary/all/results.json new file mode 100644 index 0000000..de1ecc1 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/all/results.json @@ -0,0 +1,6165 @@ +{ + "groups": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2181044.0, + "mean_tokens_input": 1087424.6667, + "mean_tokens_output": 6285.0, + "mean_cost_usd": 0.3499, + "total_cost_usd": 1.0498, + "contamination_suspect_runs": 0, + "mean_wall_s": 92.4, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1174723.0, + "mean_tokens_input": 585698.3333, + "mean_tokens_output": 3335.3333, + "mean_cost_usd": 0.7217, + "total_cost_usd": 2.1651, + "contamination_suspect_runs": 0, + "mean_wall_s": 84.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3917650.0, + "mean_tokens_input": 1953553.3333, + "mean_tokens_output": 10690.6667, + "mean_cost_usd": 0.6908, + "total_cost_usd": 2.0723, + "contamination_suspect_runs": 0, + "mean_wall_s": 171.7, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 2666065.0, + "mean_tokens_input": 1330012.6667, + "mean_tokens_output": 6056.3333, + "mean_cost_usd": 1.311, + "total_cost_usd": 3.933, + "contamination_suspect_runs": 1, + "mean_wall_s": 116.8667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3346429.3333, + "mean_tokens_input": 1667308.3333, + "mean_tokens_output": 11945.6667, + "mean_cost_usd": 0.9238, + "total_cost_usd": 2.7713, + "contamination_suspect_runs": 1, + "mean_wall_s": 193.0, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 1 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2497066.3333, + "mean_tokens_input": 1245344.0, + "mean_tokens_output": 6397.0, + "mean_cost_usd": 1.3607, + "total_cost_usd": 4.082, + "contamination_suspect_runs": 1, + "mean_wall_s": 150.0, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8095, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3115526.3333, + "mean_tokens_input": 1553145.0, + "mean_tokens_output": 9355.0, + "mean_cost_usd": 0.6453, + "total_cost_usd": 1.9359, + "contamination_suspect_runs": 0, + "mean_wall_s": 126.3333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3810044.0, + "mean_tokens_input": 1900734.3333, + "mean_tokens_output": 8598.3333, + "mean_cost_usd": 2.5605, + "total_cost_usd": 7.6816, + "contamination_suspect_runs": 3, + "mean_wall_s": 180.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8889, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4008946.3333, + "mean_tokens_input": 1999834.6667, + "mean_tokens_output": 9435.0, + "mean_cost_usd": 0.814, + "total_cost_usd": 2.4421, + "contamination_suspect_runs": 3, + "mean_wall_s": 192.9667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2547843.3333, + "mean_tokens_input": 1270822.0, + "mean_tokens_output": 6218.0, + "mean_cost_usd": 2.5449, + "total_cost_usd": 7.6347, + "contamination_suspect_runs": 0, + "mean_wall_s": 165.7333, + "statuses": { + "ok": 1, + "task_fail": 1, + "cost_limit": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8333, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3804984.3333, + "mean_tokens_input": 1896807.6667, + "mean_tokens_output": 11513.6667, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0007, + "contamination_suspect_runs": 0, + "mean_wall_s": 151.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 2919397.0, + "mean_tokens_input": 1456704.3333, + "mean_tokens_output": 6007.0, + "mean_cost_usd": 3.6571, + "total_cost_usd": 10.9713, + "contamination_suspect_runs": 1, + "mean_wall_s": 160.3333, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4444, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4851110.6667, + "mean_tokens_input": 2417172.0, + "mean_tokens_output": 16957.6667, + "mean_cost_usd": 0.8155, + "total_cost_usd": 2.4465, + "contamination_suspect_runs": 0, + "mean_wall_s": 245.9333, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2201060.6667, + "mean_tokens_input": 1097202.0, + "mean_tokens_output": 6673.3333, + "mean_cost_usd": 1.7917, + "total_cost_usd": 5.3752, + "contamination_suspect_runs": 0, + "mean_wall_s": 177.4333, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 5343871.3333, + "mean_tokens_input": 2660900.6667, + "mean_tokens_output": 22267.0, + "mean_cost_usd": 1.136, + "total_cost_usd": 3.4079, + "contamination_suspect_runs": 1, + "mean_wall_s": 241.8667, + "statuses": { + "task_fail": 1, + "turn_limit": 2 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 4314617.0, + "mean_tokens_input": 2152584.6667, + "mean_tokens_output": 9472.3333, + "mean_cost_usd": 3.2137, + "total_cost_usd": 9.6411, + "contamination_suspect_runs": 0, + "mean_wall_s": 215.1667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9444, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4774489.6667, + "mean_tokens_input": 2379971.3333, + "mean_tokens_output": 14735.0, + "mean_cost_usd": 0.6048, + "total_cost_usd": 1.8143, + "contamination_suspect_runs": 3, + "mean_wall_s": 211.1667, + "statuses": { + "ok": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2901889.3333, + "mean_tokens_input": 1447849.0, + "mean_tokens_output": 6212.6667, + "mean_cost_usd": 1.3475, + "total_cost_usd": 4.0426, + "contamination_suspect_runs": 3, + "mean_wall_s": 161.5, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.8611, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.3333, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 4055158.0, + "mean_tokens_input": 2021327.0, + "mean_tokens_output": 13611.0, + "mean_cost_usd": 0.704, + "total_cost_usd": 2.112, + "contamination_suspect_runs": 2, + "mean_wall_s": 235.8, + "statuses": { + "ok": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3231194.0, + "mean_tokens_input": 1611493.0, + "mean_tokens_output": 8227.3333, + "mean_cost_usd": 2.2869, + "total_cost_usd": 6.8606, + "contamination_suspect_runs": 2, + "mean_wall_s": 263.2, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1987527.6667, + "mean_tokens_input": 991131.6667, + "mean_tokens_output": 5340.6667, + "mean_cost_usd": 0.3924, + "total_cost_usd": 1.1771, + "contamination_suspect_runs": 3, + "mean_wall_s": 81.7, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1367355.6667, + "mean_tokens_input": 681477.0, + "mean_tokens_output": 4412.6667, + "mean_cost_usd": 0.816, + "total_cost_usd": 2.448, + "contamination_suspect_runs": 0, + "mean_wall_s": 100.6, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.8889, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 3987580.0, + "mean_tokens_input": 1988030.3333, + "mean_tokens_output": 11666.6667, + "mean_cost_usd": 0.6065, + "total_cost_usd": 1.8196, + "contamination_suspect_runs": 1, + "mean_wall_s": 143.6333, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 3146989.0, + "mean_tokens_input": 1569931.3333, + "mean_tokens_output": 7145.3333, + "mean_cost_usd": 1.5151, + "total_cost_usd": 4.5454, + "contamination_suspect_runs": 1, + "mean_wall_s": 151.6, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3316231.0, + "mean_tokens_input": 1652526.6667, + "mean_tokens_output": 11305.3333, + "mean_cost_usd": 0.7105, + "total_cost_usd": 2.1316, + "contamination_suspect_runs": 3, + "mean_wall_s": 168.1667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171259.0, + "mean_tokens_input": 583511.6667, + "mean_tokens_output": 4245.3333, + "mean_cost_usd": 0.8629, + "total_cost_usd": 2.5888, + "contamination_suspect_runs": 0, + "mean_wall_s": 99.0667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1, + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 2904388.3333, + "mean_tokens_input": 1447815.6667, + "mean_tokens_output": 8862.3333, + "mean_cost_usd": 0.5251, + "total_cost_usd": 1.5754, + "contamination_suspect_runs": 1, + "mean_wall_s": 116.9333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1912694.3333, + "mean_tokens_input": 954439.6667, + "mean_tokens_output": 3828.0, + "mean_cost_usd": 0.8633, + "total_cost_usd": 2.5899, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.4667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2514274.3333, + "mean_tokens_input": 1254177.6667, + "mean_tokens_output": 6016.6667, + "mean_cost_usd": 0.4473, + "total_cost_usd": 1.342, + "contamination_suspect_runs": 3, + "mean_wall_s": 115.9, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1248596.6667, + "mean_tokens_input": 622768.3333, + "mean_tokens_output": 3070.3333, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0005, + "contamination_suspect_runs": 0, + "mean_wall_s": 80.3667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 1681846.0, + "mean_tokens_input": 838716.6667, + "mean_tokens_output": 4472.3333, + "mean_cost_usd": 0.3867, + "total_cost_usd": 1.1601, + "contamination_suspect_runs": 2, + "mean_wall_s": 58.8, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 2208793.6667, + "mean_tokens_input": 1101807.6667, + "mean_tokens_output": 5193.6667, + "mean_cost_usd": 1.1123, + "total_cost_usd": 3.3369, + "contamination_suspect_runs": 3, + "mean_wall_s": 107.5667, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4286, + "mean_secondary_score": 0.2222, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3891013.6667, + "mean_tokens_input": 1938064.6667, + "mean_tokens_output": 15034.0, + "mean_cost_usd": 0.6625, + "total_cost_usd": 1.9874, + "contamination_suspect_runs": 1, + "mean_wall_s": 200.9667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.4444, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3999267.3333, + "mean_tokens_input": 1994081.6667, + "mean_tokens_output": 11132.0, + "mean_cost_usd": 2.1353, + "total_cost_usd": 6.406, + "contamination_suspect_runs": 1, + "mean_wall_s": 246.5, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.5556, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 4634767.3333, + "mean_tokens_input": 2310775.3333, + "mean_tokens_output": 13391.0, + "mean_cost_usd": 0.7076, + "total_cost_usd": 2.1227, + "contamination_suspect_runs": 3, + "mean_wall_s": 157.6667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 5069474.3333, + "mean_tokens_input": 2529512.3333, + "mean_tokens_output": 10480.6667, + "mean_cost_usd": 2.2438, + "total_cost_usd": 6.7314, + "contamination_suspect_runs": 3, + "mean_wall_s": 198.3667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5333, + "mean_secondary_score": 0.2222, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3253703.6667, + "mean_tokens_input": 1620560.0, + "mean_tokens_output": 12716.0, + "mean_cost_usd": 0.5715, + "total_cost_usd": 1.7144, + "contamination_suspect_runs": 1, + "mean_wall_s": 162.7, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8, + "mean_secondary_score": 0.4444, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3057943.3333, + "mean_tokens_input": 1525473.3333, + "mean_tokens_output": 7018.3333, + "mean_cost_usd": 1.4796, + "total_cost_usd": 4.4388, + "contamination_suspect_runs": 1, + "mean_wall_s": 160.7667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 4281023.0, + "mean_tokens_input": 2134509.3333, + "mean_tokens_output": 12163.3333, + "mean_cost_usd": 0.6986, + "total_cost_usd": 2.0959, + "contamination_suspect_runs": 2, + "mean_wall_s": 151.6333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.8889, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 6657902.6667, + "mean_tokens_input": 3322109.3333, + "mean_tokens_output": 13724.0, + "mean_cost_usd": 2.85, + "total_cost_usd": 8.5499, + "contamination_suspect_runs": 3, + "mean_wall_s": 261.8667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4762, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5878320.3333, + "mean_tokens_input": 2930642.0, + "mean_tokens_output": 17265.6667, + "mean_cost_usd": 0.7551, + "total_cost_usd": 2.2652, + "contamination_suspect_runs": 1, + "mean_wall_s": 267.4333, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104477.0, + "mean_tokens_input": 3046252.0, + "mean_tokens_output": 12012.6667, + "mean_cost_usd": 2.413, + "total_cost_usd": 7.2389, + "contamination_suspect_runs": 0, + "mean_wall_s": 292.0667, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 5265089.3333, + "mean_tokens_input": 2626082.0, + "mean_tokens_output": 13120.3333, + "mean_cost_usd": 0.6816, + "total_cost_usd": 2.0447, + "contamination_suspect_runs": 2, + "mean_wall_s": 151.6667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 6907009.3333, + "mean_tokens_input": 3447442.6667, + "mean_tokens_output": 12165.0, + "mean_cost_usd": 3.0569, + "total_cost_usd": 9.1706, + "contamination_suspect_runs": 3, + "mean_wall_s": 240.4667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3382552.0, + "mean_tokens_input": 1684993.3333, + "mean_tokens_output": 12698.3333, + "mean_cost_usd": 0.4741, + "total_cost_usd": 1.4222, + "contamination_suspect_runs": 3, + "mean_wall_s": 171.5667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5673125.6667, + "mean_tokens_input": 2829871.3333, + "mean_tokens_output": 13420.0, + "mean_cost_usd": 2.5848, + "total_cost_usd": 7.7545, + "contamination_suspect_runs": 0, + "mean_wall_s": 307.5333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.4444, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 5589411.6667, + "mean_tokens_input": 2786729.6667, + "mean_tokens_output": 16157.3333, + "mean_cost_usd": 0.809, + "total_cost_usd": 2.4269, + "contamination_suspect_runs": 2, + "mean_wall_s": 199.7333, + "statuses": { + "task_fail": 2, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8571, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 6553546.0, + "mean_tokens_input": 3270860.6667, + "mean_tokens_output": 11864.0, + "mean_cost_usd": 2.6164, + "total_cost_usd": 7.8493, + "contamination_suspect_runs": 3, + "mean_wall_s": 233.7667, + "statuses": { + "turn_limit": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.3333, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5389009.0, + "mean_tokens_input": 2686409.3333, + "mean_tokens_output": 16401.3333, + "mean_cost_usd": 0.8271, + "total_cost_usd": 2.4814, + "contamination_suspect_runs": 1, + "mean_wall_s": 249.8333, + "statuses": { + "task_fail": 2, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104354.6667, + "mean_tokens_input": 3046435.3333, + "mean_tokens_output": 11525.0, + "mean_cost_usd": 2.7417, + "total_cost_usd": 8.225, + "contamination_suspect_runs": 0, + "mean_wall_s": 316.8667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 4453910.3333, + "mean_tokens_input": 2221128.3333, + "mean_tokens_output": 11818.3333, + "mean_cost_usd": 0.7281, + "total_cost_usd": 2.1844, + "contamination_suspect_runs": 3, + "mean_wall_s": 237.1333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 6337522.0, + "mean_tokens_input": 3162484.0, + "mean_tokens_output": 12592.0, + "mean_cost_usd": 2.7261, + "total_cost_usd": 8.1782, + "contamination_suspect_runs": 3, + "mean_wall_s": 241.8, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4074, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2678791.0, + "mean_tokens_input": 1333450.3333, + "mean_tokens_output": 11995.6667, + "mean_cost_usd": 0.54, + "total_cost_usd": 1.6201, + "contamination_suspect_runs": 1, + "mean_wall_s": 150.7, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1818863.6667, + "mean_tokens_input": 907147.6667, + "mean_tokens_output": 4581.3333, + "mean_cost_usd": 1.118, + "total_cost_usd": 3.3541, + "contamination_suspect_runs": 0, + "mean_wall_s": 107.8333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.6296, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3026350.6667, + "mean_tokens_input": 1506925.3333, + "mean_tokens_output": 12615.0, + "mean_cost_usd": 0.5554, + "total_cost_usd": 1.6661, + "contamination_suspect_runs": 1, + "mean_wall_s": 144.9333, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3569779.3333, + "mean_tokens_input": 1780953.3333, + "mean_tokens_output": 7895.0, + "mean_cost_usd": 1.8833, + "total_cost_usd": 5.6498, + "contamination_suspect_runs": 3, + "mean_wall_s": 160.3, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.6111, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3885827.0, + "mean_tokens_input": 1933925.3333, + "mean_tokens_output": 18127.0, + "mean_cost_usd": 0.7777, + "total_cost_usd": 2.333, + "contamination_suspect_runs": 2, + "mean_wall_s": 225.1667, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1743515.6667, + "mean_tokens_input": 869117.3333, + "mean_tokens_output": 5294.3333, + "mean_cost_usd": 0.9925, + "total_cost_usd": 2.9774, + "contamination_suspect_runs": 1, + "mean_wall_s": 134.0333, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 4057816.6667, + "mean_tokens_input": 2023500.0, + "mean_tokens_output": 10970.0, + "mean_cost_usd": 0.581, + "total_cost_usd": 1.7431, + "contamination_suspect_runs": 1, + "mean_wall_s": 139.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3641256.3333, + "mean_tokens_input": 1816170.0, + "mean_tokens_output": 8938.6667, + "mean_cost_usd": 1.8291, + "total_cost_usd": 5.4873, + "contamination_suspect_runs": 2, + "mean_wall_s": 171.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.4286, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3275582.0, + "mean_tokens_input": 1632436.0, + "mean_tokens_output": 10835.0, + "mean_cost_usd": 0.505, + "total_cost_usd": 1.5151, + "contamination_suspect_runs": 2, + "mean_wall_s": 153.5, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171232.0, + "mean_tokens_input": 583887.0, + "mean_tokens_output": 3467.6667, + "mean_cost_usd": 0.8437, + "total_cost_usd": 2.531, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1716857.3333, + "mean_tokens_input": 855214.0, + "mean_tokens_output": 6491.3333, + "mean_cost_usd": 0.3704, + "total_cost_usd": 1.1112, + "contamination_suspect_runs": 0, + "mean_wall_s": 86.9, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3006424.6667, + "mean_tokens_input": 1499916.3333, + "mean_tokens_output": 6611.0, + "mean_cost_usd": 1.5832, + "total_cost_usd": 4.7496, + "contamination_suspect_runs": 1, + "mean_wall_s": 141.0, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4750311.6667, + "mean_tokens_input": 2366086.6667, + "mean_tokens_output": 18311.6667, + "mean_cost_usd": 0.7134, + "total_cost_usd": 2.1401, + "contamination_suspect_runs": 1, + "mean_wall_s": 226.3667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3606247.0, + "mean_tokens_input": 1796701.0, + "mean_tokens_output": 12870.0, + "mean_cost_usd": 2.0389, + "total_cost_usd": 6.1167, + "contamination_suspect_runs": 0, + "mean_wall_s": 250.6, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 5279550.6667, + "mean_tokens_input": 2628000.6667, + "mean_tokens_output": 23743.6667, + "mean_cost_usd": 0.8906, + "total_cost_usd": 2.6718, + "contamination_suspect_runs": 0, + "mean_wall_s": 249.9, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3734954.3333, + "mean_tokens_input": 1861290.3333, + "mean_tokens_output": 12395.6667, + "mean_cost_usd": 2.2487, + "total_cost_usd": 6.7461, + "contamination_suspect_runs": 0, + "mean_wall_s": 235.2, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.125, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4753905.3333, + "mean_tokens_input": 2369198.0, + "mean_tokens_output": 15689.3333, + "mean_cost_usd": 0.762, + "total_cost_usd": 2.286, + "contamination_suspect_runs": 1, + "mean_wall_s": 223.8333, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3480149.6667, + "mean_tokens_input": 1734587.6667, + "mean_tokens_output": 10999.6667, + "mean_cost_usd": 1.8616, + "total_cost_usd": 5.5848, + "contamination_suspect_runs": 3, + "mean_wall_s": 237.1, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 4280156.3333, + "mean_tokens_input": 2132458.6667, + "mean_tokens_output": 16319.3333, + "mean_cost_usd": 0.6932, + "total_cost_usd": 2.0796, + "contamination_suspect_runs": 2, + "mean_wall_s": 171.6667, + "statuses": { + "ok": 2, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3158432.0, + "mean_tokens_input": 1574377.6667, + "mean_tokens_output": 9697.0, + "mean_cost_usd": 1.5568, + "total_cost_usd": 4.6704, + "contamination_suspect_runs": 0, + "mean_wall_s": 157.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 0.75, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4539845.0, + "mean_tokens_input": 2263375.0, + "mean_tokens_output": 13267.6667, + "mean_cost_usd": 0.7303, + "total_cost_usd": 2.191, + "contamination_suspect_runs": 0, + "mean_wall_s": 223.7, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2242228.3333, + "mean_tokens_input": 1117914.0, + "mean_tokens_output": 6417.3333, + "mean_cost_usd": 1.306, + "total_cost_usd": 3.918, + "contamination_suspect_runs": 0, + "mean_wall_s": 153.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9583, + "mean_secondary_score": 1.0, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 3467477.6667, + "mean_tokens_input": 1729058.3333, + "mean_tokens_output": 10344.3333, + "mean_cost_usd": 0.5968, + "total_cost_usd": 1.7904, + "contamination_suspect_runs": 0, + "mean_wall_s": 132.2667, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 2368378.6667, + "mean_tokens_input": 1181231.6667, + "mean_tokens_output": 5931.0, + "mean_cost_usd": 1.4462, + "total_cost_usd": 4.3385, + "contamination_suspect_runs": 0, + "mean_wall_s": 122.2333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5417, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4584002.6667, + "mean_tokens_input": 2279746.3333, + "mean_tokens_output": 24678.6667, + "mean_cost_usd": 0.7666, + "total_cost_usd": 2.2999, + "contamination_suspect_runs": 0, + "mean_wall_s": 265.1333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9167, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3849153.3333, + "mean_tokens_input": 1916419.0, + "mean_tokens_output": 16342.0, + "mean_cost_usd": 2.1208, + "total_cost_usd": 6.3623, + "contamination_suspect_runs": 1, + "mean_wall_s": 294.4667, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.25, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 6091146.0, + "mean_tokens_input": 3032558.0, + "mean_tokens_output": 27575.3333, + "mean_cost_usd": 0.9306, + "total_cost_usd": 2.7918, + "contamination_suspect_runs": 0, + "mean_wall_s": 270.6667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 4621712.3333, + "mean_tokens_input": 2303189.6667, + "mean_tokens_output": 15360.6667, + "mean_cost_usd": 2.6001, + "total_cost_usd": 7.8003, + "contamination_suspect_runs": 1, + "mean_wall_s": 618.1667, + "statuses": { + "ok": 3 + }, + "retries": 0 + } + ], + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 39, + "n_valid": 39, + "n_excluded": 0, + "pass_rate": 0.359, + "mean_primary_score": 0.6643, + "mean_secondary_score": 0.6944, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3886943.7692, + "mean_tokens_input": 1937126.6154, + "mean_tokens_output": 12841.2821, + "mean_cost_usd": 0.6431, + "total_cost_usd": 25.0807, + "contamination_suspect_runs": 19, + "mean_wall_s": 186.6692, + "statuses": { + "ok": 14, + "task_fail": 18, + "turn_limit": 7 + }, + "retries": 1 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 39, + "n_valid": 39, + "n_excluded": 0, + "pass_rate": 0.5641, + "mean_primary_score": 0.8063, + "mean_secondary_score": 0.7179, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2823059.8974, + "mean_tokens_input": 1407961.8718, + "mean_tokens_output": 7156.3846, + "mean_cost_usd": 1.4556, + "total_cost_usd": 56.7688, + "contamination_suspect_runs": 9, + "mean_wall_s": 169.6487, + "statuses": { + "ok": 22, + "task_fail": 12, + "turn_limit": 5 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 39, + "n_valid": 39, + "n_excluded": 0, + "pass_rate": 0.7949, + "mean_primary_score": 0.9083, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.8974, + "skills_used": { + "authoring-airflow-dags": 13, + "migrating-to-airflow-3": 15, + "testing-airflow-dags": 7 + }, + "mean_tokens_total": 3812280.1282, + "mean_tokens_input": 1900363.359, + "mean_tokens_output": 11905.1026, + "mean_cost_usd": 0.6364, + "total_cost_usd": 24.8212, + "contamination_suspect_runs": 15, + "mean_wall_s": 158.1897, + "statuses": { + "ok": 31, + "task_fail": 6, + "turn_limit": 2 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 39, + "n_valid": 39, + "n_excluded": 0, + "pass_rate": 0.9487, + "mean_primary_score": 0.9927, + "mean_secondary_score": 0.906, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 15, + "migrating-to-airflow-3": 15, + "testing-airflow-dags": 9 + }, + "mean_tokens_total": 3913458.8974, + "mean_tokens_input": 1952230.8462, + "mean_tokens_output": 9021.1795, + "mean_cost_usd": 2.0064, + "total_cost_usd": 78.2482, + "contamination_suspect_runs": 22, + "mean_wall_s": 183.8667, + "statuses": { + "ok": 37, + "task_fail": 2 + }, + "retries": 1 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 21, + "n_valid": 21, + "n_excluded": 0, + "pass_rate": 0.1905, + "mean_primary_score": 0.5254, + "mean_secondary_score": 0.7143, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3816092.4762, + "mean_tokens_input": 1900826.8571, + "mean_tokens_output": 14586.381, + "mean_cost_usd": 0.6833, + "total_cost_usd": 14.3498, + "contamination_suspect_runs": 11, + "mean_wall_s": 199.3476, + "statuses": { + "ok": 4, + "turn_limit": 2, + "task_fail": 15 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 21, + "n_valid": 21, + "n_excluded": 0, + "pass_rate": 0.381, + "mean_primary_score": 0.8022, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3037224.7143, + "mean_tokens_input": 1514150.7619, + "mean_tokens_output": 8944.5714, + "mean_cost_usd": 1.8798, + "total_cost_usd": 39.4756, + "contamination_suspect_runs": 2, + "mean_wall_s": 199.7952, + "statuses": { + "ok": 8, + "task_fail": 12, + "cost_limit": 1 + }, + "retries": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 21, + "n_valid": 21, + "n_excluded": 0, + "pass_rate": 0.2381, + "mean_primary_score": 0.6821, + "mean_secondary_score": 0.7976, + "trigger_rate": 0.8095, + "skills_used": { + "authoring-airflow-dags": 7, + "migrating-to-airflow-3": 9, + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 4484988.5238, + "mean_tokens_input": 2234644.619, + "mean_tokens_output": 16054.5238, + "mean_cost_usd": 0.8091, + "total_cost_usd": 16.9914, + "contamination_suspect_runs": 8, + "mean_wall_s": 183.3619, + "statuses": { + "ok": 5, + "task_fail": 10, + "turn_limit": 6 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 21, + "n_valid": 21, + "n_excluded": 0, + "pass_rate": 0.8095, + "mean_primary_score": 0.9748, + "mean_secondary_score": 0.9683, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 12, + "migrating-to-airflow-3": 6, + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 4137317.1905, + "mean_tokens_input": 2064034.9524, + "mean_tokens_output": 9272.5238, + "mean_cost_usd": 2.4397, + "total_cost_usd": 51.2329, + "contamination_suspect_runs": 11, + "mean_wall_s": 238.5095, + "statuses": { + "ok": 17, + "turn_limit": 1, + "task_fail": 3 + }, + "retries": 1 + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.4359, + "ci95_delta_pass_rate": [ + 0.1795, + 0.6667 + ], + "mean_delta_primary_score": 0.2439, + "ci95_delta_primary_score": [ + 0.0773, + 0.4044 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-clickstream-claim-check", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.3334, + "delta_primary_score": -0.0953 + }, + { + "case": "authoring-vendor-shipments-etl", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.0833 + }, + { + "case": "debugging-manual-trigger-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.1905 + }, + { + "case": "debugging-variable-at-parse", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "migration-inventory-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.4667 + }, + { + "case": "migration-revenue-previous-day", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.5238 + }, + { + "case": "migration-weekly-catchup", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.6667 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.3889 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.5714 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.2917 + }, + { + "case": "testing-custom-operator-conn", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.5833 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.25 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.0476, + "ci95_delta_pass_rate": [ + 0.0, + 0.1428 + ], + "mean_delta_primary_score": 0.1567, + "ci95_delta_primary_score": [ + -0.0675, + 0.415 + ], + "cases": [ + { + "case": "authoring-daily-revenue-yesterday", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.0556 + }, + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.2223 + }, + { + "case": "debugging-nondeterministic-parse", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "migration-hourly-pageviews-templates", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.1428 + }, + { + "case": "migration-shipments-assets", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.8571 + }, + { + "case": "scheduling-backfill-pageviews", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.2222 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.2917 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.3846, + "ci95_delta_pass_rate": [ + 0.1538, + 0.6154 + ], + "mean_delta_primary_score": 0.1863, + "ci95_delta_primary_score": [ + 0.0672, + 0.3517 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-clickstream-claim-check", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-vendor-shipments-etl", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-manual-trigger-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.1429 + }, + { + "case": "debugging-variable-at-parse", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 1.0 + }, + { + "case": "migration-inventory-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2 + }, + { + "case": "migration-revenue-previous-day", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2381 + }, + { + "case": "migration-weekly-catchup", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2857 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.2222 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.3333 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "testing-custom-operator-conn", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.4286, + "ci95_delta_pass_rate": [ + 0.0476, + 0.7619 + ], + "mean_delta_primary_score": 0.1727, + "ci95_delta_primary_score": [ + 0.0627, + 0.288 + ], + "cases": [ + { + "case": "authoring-daily-revenue-yesterday", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.2222 + }, + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-nondeterministic-parse", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.0333 + }, + { + "case": "migration-hourly-pageviews-templates", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.4286 + }, + { + "case": "migration-shipments-assets", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.2857 + }, + { + "case": "scheduling-backfill-pageviews", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2222 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.0833 + } + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 13, + "n_pairs": 26, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.4103, + "ci95_delta_pass_rate": [ + 0.2051, + 0.6154 + ], + "mean_delta_primary_score": 0.2151, + "ci95_delta_primary_score": [ + 0.0974, + 0.3263 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 7, + "n_pairs": 14, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2381, + "ci95_delta_pass_rate": [ + 0.0238, + 0.4286 + ], + "mean_delta_primary_score": 0.1647, + "ci95_delta_primary_score": [ + 0.0231, + 0.3235 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ], + "failure_classes": { + "ok": 138, + "task_fail": 78, + "turn_limit": 23, + "cost_limit": 1 + }, + "total_cost_usd": 306.9687, + "isolation": { + "contamination_suspects": [ + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-yesterday-pyn34e58/ws/README.md", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/first_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/second_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/second_run.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_before.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_after.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_before.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_after.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SOLUTION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SOLUTION.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow-test-", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow-test-", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_verification.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_verification.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/detailed_changes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/detailed_changes.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/inv_test", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/dags/sql/daily_revenue.sql", + "root": "work-root" + }, + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/data/orders.csv", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/MIGRATION_CHECKLIST.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/MIGRATION_CHECKLIST.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/CHANGES_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SCHEDULE_DIAGRAM.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SCHEDULE_DIAGRAM.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/analyze_shipments.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/analyze_shipments.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb2.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "write", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", + "root": "work-root" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/landing", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/debugging-variable-at-parse-y19jdpne/ws/dags/warehouse_load.py", + "root": "work-root" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_after.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_after.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow-test-", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/dag_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/dag_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/authoring-daily-revenue-yesterday-ew4q6g1i/ws", + "root": "work-root" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/landing", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/debugging-manual-trigger-context-1x63tzof/ws/dags/partner_feed.py", + "root": "work-root" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "edit", + "path": "/baseline-run3-2026-09-30-baseline-20260930-debugging-variable-at-parse-ldmu9aj0/ws/dags/warehouse_load.py", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-082417", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/airflow_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/upgrade_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/upgrade_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-182417/migration-inventory-context-im02tn8a/ws/dags/inventory_snapshot.py", + "root": "work-root" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/migration-shipments-assets-bz1624jc/ws/dags/carrier_scorecard.py", + "root": "work-root" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/pageviews_setup_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/pageviews_setup_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_pageviews_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_pageviews_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/QUICK_REFERENCE.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/QUICK_REFERENCE.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CRITICAL_PATTERNS.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_pageviews_idempotency.py", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/dag_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/dag_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fx_pnl_changes_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fx_pnl_changes_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/deployment_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/deployment_guide.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "////airflow_vendor_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow_vendor_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow_vendor_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home2_", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_warehouse", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/wh_test_dir", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/wh_test_dir", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_notes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_notes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_baseline.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_baseline.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after_verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after_verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/FINAL_MIGRATION_REPORT.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/FINAL_MIGRATION_REPORT.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_airflow", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_1", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_rerun_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "turn_limit", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": ".nymKSGkNb1", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/requirements_checklist.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/requirements_checklist.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/usage_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/usage_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/DELIVERY_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/DELIVERY_SUMMARY.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag_idempotent.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag_idempotent.sh", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_inv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/snap_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/low_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/snap_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/low_before.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/first_run_2026-09-29.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-29.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_warehouse", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/ci_warehouse", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/ci_warehouse/warehouse.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_pageviews_backfill.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_pageviews_backfill.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/BACKFILL_SETUP_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/BACKFILL_SETUP_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/VERIFICATION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/VERIFICATION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CONCERN_ANALYSIS.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CONCERN_ANALYSIS.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "turn_limit", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp" + } + ] + } + ], + "kill_command_runs": [], + "agent_env_restored_runs": [], + "agent_env_restore_failed_runs": [] + }, + "sources": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30" + }, + { + "dir": "evals/airflow/results/baseline-run3-2026-09-30" + }, + { + "dir": "evals/airflow/results/skill-2026-09-30" + } + ], + "n_runs": 240 +} \ No newline at end of file diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/contamination.jsonl b/evals/airflow/results/altimate-code-2026-09-30-summary/contamination.jsonl new file mode 100644 index 0000000..f0632e8 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/contamination.jsonl @@ -0,0 +1,244 @@ +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 1, "shared-temp": 4}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "read", "path": "/baseline-2026-09-30-baseline-20260930-yesterday-pyn34e58/ws/README.md", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/second_run.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/second_run.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/partner_feed_before.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/partner_feed_after.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/partner_feed_before.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/partner_feed_after.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/SOLUTION.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/SOLUTION.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow-test-", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "////airflow-test-", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/vendor_shipments_verification.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/vendor_shipments_verification.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 3}, "n_hits": 3, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/warehouse", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 5, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/detailed_changes.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/detailed_changes.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 1, "hits": [{"tool": "bash", "path": "/inv_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 2, "shared-temp": 4}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "read", "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/dags/sql/daily_revenue.sql", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "read", "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/data/orders.csv", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/MIGRATION_CHECKLIST.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/MIGRATION_CHECKLIST.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 2, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/CHANGES_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/SCHEDULE_DIAGRAM.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/SCHEDULE_DIAGRAM.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/analyze_shipments.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/analyze_shipments.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test.db", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test.db", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test.db", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/testdb.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/testdb.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/testdb.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/testdb2.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "write", "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "infra_error", "scored": false, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": false, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 1, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/landing", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "read", "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/debugging-variable-at-parse-y19jdpne/ws/dags/warehouse_load.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before_after.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before_after.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 7, "hits": [{"tool": "bash", "path": "", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/verification.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/verification.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow-test-", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-idempotent", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-idempotent", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-idempotent", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-idempotent", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "baseline-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/dag_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/dag_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/authoring-daily-revenue-yesterday-ew4q6g1i/ws", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "cost_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "infra_error", "scored": false, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": false, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 1, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/landing", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 9}, "n_hits": 9, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "read", "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/debugging-manual-trigger-context-1x63tzof/ws/dags/partner_feed.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 4, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 4, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"work-root": 2, "shared-temp": 2}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "edit", "path": "/baseline-run3-2026-09-30-baseline-20260930-debugging-variable-at-parse-ldmu9aj0/ws/dags/warehouse_load.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/baseline-run3-2026-09-30-baseline-20260930-082417", "root": "work-root", "first_access": "use", "strength": "strong", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 5, "hits": [], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2, "work-root": 1}, "n_hits": 3, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/upgrade_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/upgrade_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/baseline-run3-2026-09-30-baseline-20260930-182417/migration-inventory-context-im02tn8a/ws/dags/inventory_snapshot.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 3, "hits": [], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"work-root": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "read", "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/migration-shipments-assets-bz1624jc/ws/dags/carrier_scorecard.py", "root": "work-root", "first_access": "own_scratch_mistyped", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 7, "hits": [], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 12}, "n_hits": 12, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/pageviews_setup_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/pageviews_setup_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_pageviews_idempotency.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_pageviews_idempotency.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/QUICK_REFERENCE.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/QUICK_REFERENCE.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CRITICAL_PATTERNS.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_pageviews_idempotency.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CRITICAL_PATTERNS.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 1, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/dag_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/dag_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/fx_pnl_changes_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/fx_pnl_changes_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test.duckdb", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "baseline-run3-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 15}, "n_hits": 15, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_csp", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_csp", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_csp", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_csp", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "infra_error", "scored": false, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 1, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_dr", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_dr", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_dr2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_dr2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_dr2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/deployment_guide.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/deployment_guide.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "////airflow_vendor_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_vendor_home_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "////airflow_vendor_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_vendor_home2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "////airflow_vendor_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_vendor_home2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_warehouse", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "infra_error", "scored": false, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": false, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 1, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/wh_test_dir", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/wh_test_dir", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 3}, "n_hits": 3, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 2, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 15}, "n_hits": 15, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 4, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 4, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 3, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 4, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 4, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 4, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 5, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_notes.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_notes.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before_baseline.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before_baseline.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/after_verification.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/after_verification.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/FINAL_MIGRATION_REPORT.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/FINAL_MIGRATION_REPORT.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 6, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 2, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 9, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 9, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 7, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 9, "hits": [{"tool": "bash", "path": "/airflow_test_pv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_pv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 3, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/af_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_airflow", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 11}, "n_hits": 11, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 3, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 3, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_1", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run_2026-09-27.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run_2026-09-27.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run_2026-09-27.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_rerun_test", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run_2026-09-27.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_28_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_dag.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/verify_dag_structure.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/verify_dag_structure.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/verify_dag_structure.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": ".nymKSGkNb1", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 10, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/warehouse_ingest_fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/vendor_shipments_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/requirements_checklist.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/requirements_checklist.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/usage_guide.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/usage_guide.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/DELIVERY_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/DELIVERY_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 13}, "n_hits": 13, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 8, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "////test_import.db", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 11}, "n_hits": 11, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/output", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 9}, "n_hits": 9, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 9, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 9, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 10, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 10, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 10, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 15, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 15, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 11, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 13}, "n_hits": 13, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 12, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 13, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 14, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 15, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 15, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 6}, "n_hits": 6, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/af_test_pv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_pv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_pv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_pv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 3, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 3, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", "baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/verification_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/verification_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 7}, "n_hits": 7, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_dag.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag_idempotent.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_dag_idempotent.sh", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 16, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 10}, "n_hits": 10, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 4, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 4, "prior_users": ["baseline-run3-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_test_inv", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_inv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_inv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/snap_before.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/low_before.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow_inv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/snap_before.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/low_before.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/first_run_2026-09-29.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/first_run_2026-09-29.csv", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 8}, "n_hits": 8, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/orders_enrichment.py.bak", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-airflow2-inventory-snapshot", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-clickstream-claim-check", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-daily-revenue-yesterday", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-partner-feeds-mapping", "split": "holdout", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-manual-trigger-context", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 20, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "debugging-nondeterministic-parse", "split": "holdout", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "debugging-variable-at-parse", "split": "dev", "area": "debugging", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_warehouse", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 5, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/fix_summary.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 5, "prior_users": ["baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/scheduling-idempotent-orders-load/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", "baseline-run3-2026-09-30/runs/debugging-nondeterministic-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/ci_warehouse", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/ci_warehouse/warehouse.duckdb", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "authoring-vendor-shipments-etl", "split": "dev", "area": "authoring", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 18, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-hourly-pageviews-templates", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 10}, "n_hits": 10, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 17, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-inventory-context", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 14}, "n_hits": 14, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 19, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-inv", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-inv", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-inv", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/airflow-test-inv", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 15}, "n_hits": 15, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 20, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 24, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 20, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after2.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/after2.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 25, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"]}, {"tool": "bash", "path": "/after2.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-revenue-previous-day", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 2}, "n_hits": 2, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/migration_summary.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "baseline-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 5}, "n_hits": 5, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 25, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 25, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 25, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 21, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-shipments-assets", "split": "holdout", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 4}, "n_hits": 4, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "migration-weekly-catchup", "split": "dev", "area": "migration", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 17}, "n_hits": 17, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 26, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 26, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 26, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 26, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 22, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/before.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 27, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}, {"tool": "bash", "path": "/after.json", "root": "shared-temp", "first_access": "overwrite", "strength": "strong", "n_prior_users": 23, "prior_users": ["skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0"], "concurrent_writers": ["skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0"]}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 10}, "n_hits": 10, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/af_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 2, "prior_users": ["skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 2, "prior_users": ["skill-2026-09-30/runs/scheduling-weekday-ny-business-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", "skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/af_test2_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 1, "prior_users": ["skill-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0"], "concurrent_writers": []}, {"tool": "bash", "path": "/af_pgv_", "root": "shared-temp", "first_access": "generated", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_pgv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_pgv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_pgv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/af_pgv2", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-backfill-pageviews", "split": "holdout", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "task_fail", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 14}, "n_hits": 14, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test_pageviews_backfill.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_pageviews_backfill.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/BACKFILL_SETUP_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/BACKFILL_SETUP_SUMMARY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_idempotency.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/test_idempotency.py", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/VERIFICATION.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/VERIFICATION.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CONCERN_ANALYSIS.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CONCERN_ANALYSIS.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES_SUMMARY.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/CHANGES_SUMMARY.txt", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/HOW_TO_VERIFY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": "/HOW_TO_VERIFY.md", "root": "shared-temp", "first_access": "overwrite", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "weak", "hit_roots": {"shared-temp": 3}, "n_hits": 3, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": ".IKuqtA2Qn0", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": ".IKuqtA2Qn0", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}, {"tool": "bash", "path": ".IKuqtA2Qn0", "root": "shared-temp", "first_access": "use", "strength": "weak", "n_prior_users": 0, "prior_users": [], "concurrent_writers": []}], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "scheduling-idempotent-orders-load", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 1, "hits": [], "denied_hits": []} +{"case": "scheduling-weekday-ny-business-day", "split": "dev", "area": "scheduling", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-airflow2-ci-suite", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-custom-operator-conn", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": true, "evidence_strength": "strong", "hit_roots": {"shared-temp": 1}, "n_hits": 1, "scratch_inferred": true, "grader_env_refs": 0, "hits": [{"tool": "bash", "path": "/test.duckdb", "root": "shared-temp", "first_access": "use", "strength": "strong", "n_prior_users": 2, "prior_users": ["baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0", "baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0"], "concurrent_writers": []}], "denied_hits": []} +{"case": "testing-dagbag-integrity", "split": "dev", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "turn_limit", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} +{"case": "testing-taskflow-transforms", "split": "holdout", "area": "testing", "arm": "skill", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "source": "skill-2026-09-30", "attempt": 0, "status": "ok", "scored": true, "events_available": true, "contamination_suspect": false, "evidence_strength": "none", "hit_roots": {}, "n_hits": 0, "scratch_inferred": true, "grader_env_refs": 0, "hits": [], "denied_hits": []} diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/REPORT.md b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/REPORT.md new file mode 100644 index 0000000..767dba1 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/REPORT.md @@ -0,0 +1,131 @@ +# Airflow skill eval report + +Pass rate = fraction of valid runs whose primary checks all passed. Excluded runs (infra_error, grader_error, skipped_budget) are listed but not counted. + +## dev: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 20 (20) | 40% | 0.636 | 0.654 | 0% | 4.07e+06 | 0.645 | 203 | {'ok': 8, 'task_fail': 7, 'turn_limit': 5} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 30 (30) | 47% | 0.755 | 0.689 | 0% | 2.74e+06 | 1.42 | 164 | {'ok': 14, 'task_fail': 11, 'turn_limit': 5} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 24 (24) | 75% | 0.901 | 0.958 | 96% | 3.6e+06 | 0.632 | 156 | {'ok': 18, 'task_fail': 5, 'turn_limit': 1} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 17 (17) | 94% | 0.992 | 0.961 | 100% | 2.9e+06 | 1.58 | 151 | {'ok': 16, 'task_fail': 1} | + +## dev: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 9 | 0.426 | (0.0926, 0.7778) | 0.267 | (0.0299, 0.5093) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 6 | 0.222 | (0.0555, 0.3889) | 0.139 | (0.0278, 0.25) | +| pooled (15 case x model pairs) | 10 | 0.344 | (0.1146, 0.6333) | 0.215 | (0.0536, 0.4125) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## dev: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 0.667 | - | 0.35 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.722 | {'ok': 3} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 1 | {'authoring-airflow-dags': 3} | 0.691 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'authoring-airflow-dags': 2} | 1.47 | {'ok': 2} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 50% | 0.929 | 0.833 | - | 0.571 | {'task_fail': 1, 'ok': 1} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 0.667 | - | 1.25 | {'ok': 2} | +| authoring-clickstream-claim-check | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.809 | 1 | {'authoring-airflow-dags': 3} | 0.645 | {'ok': 2, 'task_fail': 1} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 0% | 0.917 | 1 | {'authoring-airflow-dags': 1} | 0.854 | {'task_fail': 1} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 1 (1) | 100% | 1 | 0.667 | {'authoring-airflow-dags': 1} | 1.37 | {'ok': 1} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 0.816 | {'task_fail': 3} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 50% | 0.857 | 0.833 | {'migrating-to-airflow-3': 2} | 0.589 | {'task_fail': 1, 'ok': 1} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 50% | 0.929 | 0.833 | {'migrating-to-airflow-3': 2} | 1.27 | {'task_fail': 1, 'ok': 1} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0 | 0.333 | - | 1 | {'task_fail': 3} | +| debugging-variable-at-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 100% | 1 | 1 | {'migrating-to-airflow-3': 1} | 0.322 | {'ok': 1} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.6 | 0.167 | - | 0.46 | {'task_fail': 2} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 0% | 0.8 | 0.333 | - | 1.53 | {'task_fail': 2} | +| migration-inventory-context | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 1} | 0.559 | {'ok': 1} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.429 | 0.667 | - | 0.765 | {'turn_limit': 2} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 2.41 | {'turn_limit': 2, 'task_fail': 1} | +| migration-revenue-previous-day | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 1} | 0.847 | {'ok': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.286 | 0.333 | - | 0.835 | {'task_fail': 1, 'turn_limit': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.714 | 0.778 | - | 2.74 | {'turn_limit': 3} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 100% | 1 | 0.667 | - | 0.917 | {'ok': 1} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 50% | 0.667 | 0.667 | - | 0.759 | {'task_fail': 1, 'ok': 1} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 1 | {'authoring-airflow-dags': 2} | 0.602 | {'ok': 2} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 1 (1) | 100% | 1 | 1 | {'authoring-airflow-dags': 1} | 1.81 | {'ok': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 0% | 0.286 | 0.667 | - | 0.665 | {'task_fail': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.667 | 0.667 | - | 0.844 | {'ok': 2, 'task_fail': 1} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 0.37 | {'ok': 3} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'authoring-airflow-dags': 2} | 1.22 | {'ok': 2} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 1 | - | 0.728 | {'ok': 2} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 2.04 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 1 | {'testing-airflow-dags': 3} | 0.891 | {'turn_limit': 1, 'ok': 2} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 2.25 | {'ok': 3} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.125 | 0.75 | - | 0.681 | {'task_fail': 1, 'turn_limit': 1} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 100% | 1 | 1 | {'testing-airflow-dags': 1} | 0.615 | {'ok': 1} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.56 | {'ok': 3} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 0.75 | - | 0.73 | {'turn_limit': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 1.31 | {'ok': 3} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.958 | 1 | {'testing-airflow-dags': 2} | 0.597 | {'task_fail': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.45 | {'ok': 3} | + +## holdout: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 10 (10) | 0% | 0.489 | 0.733 | 0% | 3.89e+06 | 0.682 | 214 | {'turn_limit': 2, 'task_fail': 8} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 19 (19) | 37% | 0.804 | 0.649 | 0% | 2.86e+06 | 1.8 | 189 | {'ok': 7, 'task_fail': 11, 'cost_limit': 1} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 13 (13) | 31% | 0.676 | 0.878 | 77% | 4.44e+06 | 0.873 | 185 | {'ok': 4, 'task_fail': 4, 'turn_limit': 5} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 10 (10) | 90% | 0.99 | 0.933 | 100% | 3.31e+06 | 1.96 | 240 | {'ok': 9, 'task_fail': 1} | + +## holdout: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 | 0.167 | (0.0, 0.5) | 0.0139 | (-0.2917, 0.2778) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 4 | 0.333 | (-0.1666, 0.8334) | 0.0785 | (-0.0167, 0.1736) | +| pooled (7 case x model pairs) | 5 | 0.262 | (-0.037, 0.5556) | 0.0508 | (-0.0492, 0.1519) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## holdout: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.778 | 0.778 | - | 2.54 | {'ok': 1, 'task_fail': 1, 'cost_limit': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.833 | 0.889 | {'authoring-airflow-dags': 2} | 1 | {'ok': 2, 'task_fail': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'authoring-airflow-dags': 2} | 1.3 | {'ok': 2} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.444 | 0.889 | - | 0.816 | {'turn_limit': 2, 'task_fail': 1} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.556 | - | 1.79 | {'ok': 3} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.722 | 0.833 | {'authoring-airflow-dags': 1} | 1.27 | {'task_fail': 1, 'turn_limit': 1} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | {'authoring-airflow-dags': 3} | 3.21 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.863 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 50% | 0.95 | 1 | {'authoring-airflow-dags': 1, 'migrating-to-airflow-3': 2} | 0.55 | {'ok': 1, 'task_fail': 1} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.967 | 1 | {'authoring-airflow-dags': 3} | 0.863 | {'ok': 2, 'task_fail': 1} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.357 | 0.167 | - | 0.643 | {'task_fail': 2} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 0% | 0.571 | 0.333 | - | 2.03 | {'task_fail': 2} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.571 | 0.556 | - | 2.58 | {'task_fail': 3} | +| migration-shipments-assets | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 0% | 0.857 | 0.333 | {'migrating-to-airflow-3': 1} | 0.75 | {'turn_limit': 1} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.611 | 0.667 | - | 0.395 | {'task_fail': 2} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.778 | 0.667 | - | 1.12 | {'task_fail': 3} | +| scheduling-backfill-pageviews | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 50% | 0.667 | 1 | {'authoring-airflow-dags': 2} | 0.585 | {'task_fail': 1, 'ok': 1} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.542 | 1 | - | 0.767 | {'task_fail': 3} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 0% | 0.875 | 1 | - | 1.7 | {'task_fail': 2} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.25 | 0.917 | {'testing-airflow-dags': 2} | 0.931 | {'turn_limit': 3} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'testing-airflow-dags': 2} | 2.39 | {'ok': 2} | + +## Failure classes (all runs) + +{'ok': 76, 'task_fail': 48, 'turn_limit': 18, 'cost_limit': 1} + +Total spend (all attempts, all dirs): $169.60 + +## Isolation + +Contamination suspects: 0 run(s). None. +Broad kill commands (pkill/killall/kill by pattern): 0 run(s). None. +Agent env restored after: 0 run(s); restore failed: 0. + +## Sources + +- `evals/airflow/results/baseline-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/baseline-run3-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/skill-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/results.json b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/results.json new file mode 100644 index 0000000..82a6684 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-flagged/results.json @@ -0,0 +1,2274 @@ +{ + "groups": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2181044.0, + "mean_tokens_input": 1087424.6667, + "mean_tokens_output": 6285.0, + "mean_cost_usd": 0.3499, + "total_cost_usd": 1.0498, + "contamination_suspect_runs": 0, + "mean_wall_s": 92.4, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1174723.0, + "mean_tokens_input": 585698.3333, + "mean_tokens_output": 3335.3333, + "mean_cost_usd": 0.7217, + "total_cost_usd": 2.1651, + "contamination_suspect_runs": 0, + "mean_wall_s": 84.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3917650.0, + "mean_tokens_input": 1953553.3333, + "mean_tokens_output": 10690.6667, + "mean_cost_usd": 0.6908, + "total_cost_usd": 2.0723, + "contamination_suspect_runs": 0, + "mean_wall_s": 171.7, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 2756296.5, + "mean_tokens_input": 1375238.0, + "mean_tokens_output": 5837.0, + "mean_cost_usd": 1.4715, + "total_cost_usd": 2.943, + "contamination_suspect_runs": 0, + "mean_wall_s": 116.2, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.9286, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3080173.5, + "mean_tokens_input": 1534294.5, + "mean_tokens_output": 11707.5, + "mean_cost_usd": 0.5705, + "total_cost_usd": 1.141, + "contamination_suspect_runs": 0, + "mean_wall_s": 218.35, + "statuses": { + "task_fail": 1, + "ok": 1 + }, + "retries": 1 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2143714.5, + "mean_tokens_input": 1068672.5, + "mean_tokens_output": 6386.0, + "mean_cost_usd": 1.2546, + "total_cost_usd": 2.5092, + "contamination_suspect_runs": 0, + "mean_wall_s": 149.85, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8095, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3115526.3333, + "mean_tokens_input": 1553145.0, + "mean_tokens_output": 9355.0, + "mean_cost_usd": 0.6453, + "total_cost_usd": 1.9359, + "contamination_suspect_runs": 0, + "mean_wall_s": 126.3333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2547843.3333, + "mean_tokens_input": 1270822.0, + "mean_tokens_output": 6218.0, + "mean_cost_usd": 2.5449, + "total_cost_usd": 7.6347, + "contamination_suspect_runs": 0, + "mean_wall_s": 165.7333, + "statuses": { + "ok": 1, + "task_fail": 1, + "cost_limit": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8333, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3804984.3333, + "mean_tokens_input": 1896807.6667, + "mean_tokens_output": 11513.6667, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0007, + "contamination_suspect_runs": 0, + "mean_wall_s": 151.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 2619867.0, + "mean_tokens_input": 1307313.5, + "mean_tokens_output": 5257.0, + "mean_cost_usd": 1.2964, + "total_cost_usd": 2.5928, + "contamination_suspect_runs": 0, + "mean_wall_s": 112.25, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4444, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4851110.6667, + "mean_tokens_input": 2417172.0, + "mean_tokens_output": 16957.6667, + "mean_cost_usd": 0.8155, + "total_cost_usd": 2.4465, + "contamination_suspect_runs": 0, + "mean_wall_s": 245.9333, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2201060.6667, + "mean_tokens_input": 1097202.0, + "mean_tokens_output": 6673.3333, + "mean_cost_usd": 1.7917, + "total_cost_usd": 5.3752, + "contamination_suspect_runs": 0, + "mean_wall_s": 177.4333, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7222, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.5, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 4908036.0, + "mean_tokens_input": 2443911.0, + "mean_tokens_output": 20398.0, + "mean_cost_usd": 1.2659, + "total_cost_usd": 2.5318, + "contamination_suspect_runs": 0, + "mean_wall_s": 227.55, + "statuses": { + "task_fail": 1, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 4314617.0, + "mean_tokens_input": 2152584.6667, + "mean_tokens_output": 9472.3333, + "mean_cost_usd": 3.2137, + "total_cost_usd": 9.6411, + "contamination_suspect_runs": 0, + "mean_wall_s": 215.1667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.9167, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 4497696.0, + "mean_tokens_input": 2243699.0, + "mean_tokens_output": 13320.0, + "mean_cost_usd": 0.854, + "total_cost_usd": 0.854, + "contamination_suspect_runs": 0, + "mean_wall_s": 402.8, + "statuses": { + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 2800094.0, + "mean_tokens_input": 1396970.0, + "mean_tokens_output": 6172.0, + "mean_cost_usd": 1.3739, + "total_cost_usd": 1.3739, + "contamination_suspect_runs": 0, + "mean_wall_s": 132.4, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1367355.6667, + "mean_tokens_input": 681477.0, + "mean_tokens_output": 4412.6667, + "mean_cost_usd": 0.816, + "total_cost_usd": 2.448, + "contamination_suspect_runs": 0, + "mean_wall_s": 100.6, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.8333, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 3187676.0, + "mean_tokens_input": 1588040.5, + "mean_tokens_output": 11713.0, + "mean_cost_usd": 0.5888, + "total_cost_usd": 1.1776, + "contamination_suspect_runs": 0, + "mean_wall_s": 135.25, + "statuses": { + "task_fail": 1, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.9286, + "mean_secondary_score": 0.8333, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 2616341.5, + "mean_tokens_input": 1305315.0, + "mean_tokens_output": 5727.0, + "mean_cost_usd": 1.2747, + "total_cost_usd": 2.5493, + "contamination_suspect_runs": 0, + "mean_wall_s": 126.2, + "statuses": { + "task_fail": 1, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171259.0, + "mean_tokens_input": 583511.6667, + "mean_tokens_output": 4245.3333, + "mean_cost_usd": 0.8629, + "total_cost_usd": 2.5888, + "contamination_suspect_runs": 0, + "mean_wall_s": 99.0667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.95, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1, + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 2970642.0, + "mean_tokens_input": 1481292.5, + "mean_tokens_output": 8166.0, + "mean_cost_usd": 0.5503, + "total_cost_usd": 1.1005, + "contamination_suspect_runs": 0, + "mean_wall_s": 115.25, + "statuses": { + "ok": 1, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1912694.3333, + "mean_tokens_input": 954439.6667, + "mean_tokens_output": 3828.0, + "mean_cost_usd": 0.8633, + "total_cost_usd": 2.5899, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.4667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1248596.6667, + "mean_tokens_input": 622768.3333, + "mean_tokens_output": 3070.3333, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0005, + "contamination_suspect_runs": 0, + "mean_wall_s": 80.3667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 1 + }, + "mean_tokens_total": 1154307.0, + "mean_tokens_input": 575637.0, + "mean_tokens_output": 3073.0, + "mean_cost_usd": 0.3216, + "total_cost_usd": 0.3216, + "contamination_suspect_runs": 0, + "mean_wall_s": 42.1, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.3571, + "mean_secondary_score": 0.1667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3328589.5, + "mean_tokens_input": 1656988.5, + "mean_tokens_output": 14741.5, + "mean_cost_usd": 0.6434, + "total_cost_usd": 1.2869, + "contamination_suspect_runs": 0, + "mean_wall_s": 175.75, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3776757.0, + "mean_tokens_input": 1882916.5, + "mean_tokens_output": 10950.5, + "mean_cost_usd": 2.0262, + "total_cost_usd": 4.0524, + "contamination_suspect_runs": 0, + "mean_wall_s": 226.95, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.6, + "mean_secondary_score": 0.1667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3018427.0, + "mean_tokens_input": 1503944.0, + "mean_tokens_output": 10662.5, + "mean_cost_usd": 0.4603, + "total_cost_usd": 0.9205, + "contamination_suspect_runs": 0, + "mean_wall_s": 158.9, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3048015.0, + "mean_tokens_input": 1520569.0, + "mean_tokens_output": 6899.5, + "mean_cost_usd": 1.528, + "total_cost_usd": 3.056, + "contamination_suspect_runs": 0, + "mean_wall_s": 149.65, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 1 + }, + "mean_tokens_total": 4104582.0, + "mean_tokens_input": 2046305.0, + "mean_tokens_output": 12126.0, + "mean_cost_usd": 0.5587, + "total_cost_usd": 0.5587, + "contamination_suspect_runs": 0, + "mean_wall_s": 142.3, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4286, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5915216.5, + "mean_tokens_input": 2948813.0, + "mean_tokens_output": 17819.5, + "mean_cost_usd": 0.7655, + "total_cost_usd": 1.531, + "contamination_suspect_runs": 0, + "mean_wall_s": 251.1, + "statuses": { + "turn_limit": 2 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104477.0, + "mean_tokens_input": 3046252.0, + "mean_tokens_output": 12012.6667, + "mean_cost_usd": 2.413, + "total_cost_usd": 7.2389, + "contamination_suspect_runs": 0, + "mean_wall_s": 292.0667, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 1 + }, + "mean_tokens_total": 6050669.0, + "mean_tokens_input": 3017406.0, + "mean_tokens_output": 16083.0, + "mean_cost_usd": 0.8473, + "total_cost_usd": 0.8473, + "contamination_suspect_runs": 0, + "mean_wall_s": 176.4, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5673125.6667, + "mean_tokens_input": 2829871.3333, + "mean_tokens_output": 13420.0, + "mean_cost_usd": 2.5848, + "total_cost_usd": 7.7545, + "contamination_suspect_runs": 0, + "mean_wall_s": 307.5333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.3333, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 1 + }, + "mean_tokens_total": 6070554.0, + "mean_tokens_input": 3027599.0, + "mean_tokens_output": 15571.0, + "mean_cost_usd": 0.7504, + "total_cost_usd": 0.7504, + "contamination_suspect_runs": 0, + "mean_wall_s": 176.3, + "statuses": { + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.2857, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5194371.0, + "mean_tokens_input": 2588554.0, + "mean_tokens_output": 17465.5, + "mean_cost_usd": 0.8353, + "total_cost_usd": 1.6706, + "contamination_suspect_runs": 0, + "mean_wall_s": 225.85, + "statuses": { + "task_fail": 1, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104354.6667, + "mean_tokens_input": 3046435.3333, + "mean_tokens_output": 11525.0, + "mean_cost_usd": 2.7417, + "total_cost_usd": 8.225, + "contamination_suspect_runs": 0, + "mean_wall_s": 316.8667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.6111, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1990823.0, + "mean_tokens_input": 991262.0, + "mean_tokens_output": 8376.0, + "mean_cost_usd": 0.3948, + "total_cost_usd": 0.7895, + "contamination_suspect_runs": 0, + "mean_wall_s": 129.85, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1818863.6667, + "mean_tokens_input": 907147.6667, + "mean_tokens_output": 4581.3333, + "mean_cost_usd": 1.118, + "total_cost_usd": 3.3541, + "contamination_suspect_runs": 0, + "mean_wall_s": 107.8333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.6667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3119896.5, + "mean_tokens_input": 1553877.5, + "mean_tokens_output": 12259.0, + "mean_cost_usd": 0.5852, + "total_cost_usd": 1.1704, + "contamination_suspect_runs": 0, + "mean_wall_s": 140.25, + "statuses": { + "task_fail": 1, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4173592.0, + "mean_tokens_input": 2072330.0, + "mean_tokens_output": 29095.0, + "mean_cost_usd": 0.9175, + "total_cost_usd": 0.9175, + "contamination_suspect_runs": 0, + "mean_wall_s": 338.0, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.5, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1383601.5, + "mean_tokens_input": 689232.5, + "mean_tokens_output": 5147.5, + "mean_cost_usd": 0.759, + "total_cost_usd": 1.518, + "contamination_suspect_runs": 0, + "mean_wall_s": 112.5, + "statuses": { + "task_fail": 1, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 4200411.0, + "mean_tokens_input": 2094730.5, + "mean_tokens_output": 11109.5, + "mean_cost_usd": 0.6018, + "total_cost_usd": 1.2036, + "contamination_suspect_runs": 0, + "mean_wall_s": 142.4, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 3688026.0, + "mean_tokens_input": 1839600.0, + "mean_tokens_output": 8849.0, + "mean_cost_usd": 1.8148, + "total_cost_usd": 1.8148, + "contamination_suspect_runs": 0, + "mean_wall_s": 168.4, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.2857, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4858432.0, + "mean_tokens_input": 2422364.0, + "mean_tokens_output": 13888.0, + "mean_cost_usd": 0.6646, + "total_cost_usd": 0.6646, + "contamination_suspect_runs": 0, + "mean_wall_s": 215.3, + "statuses": { + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171232.0, + "mean_tokens_input": 583887.0, + "mean_tokens_output": 3467.6667, + "mean_cost_usd": 0.8437, + "total_cost_usd": 2.531, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1716857.3333, + "mean_tokens_input": 855214.0, + "mean_tokens_output": 6491.3333, + "mean_cost_usd": 0.3704, + "total_cost_usd": 1.1112, + "contamination_suspect_runs": 0, + "mean_wall_s": 86.9, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 2108546.0, + "mean_tokens_input": 1051363.5, + "mean_tokens_output": 5833.0, + "mean_cost_usd": 1.2189, + "total_cost_usd": 2.4379, + "contamination_suspect_runs": 0, + "mean_wall_s": 117.3, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4709330.5, + "mean_tokens_input": 2344748.0, + "mean_tokens_output": 20005.5, + "mean_cost_usd": 0.7282, + "total_cost_usd": 1.4565, + "contamination_suspect_runs": 0, + "mean_wall_s": 224.9, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3606247.0, + "mean_tokens_input": 1796701.0, + "mean_tokens_output": 12870.0, + "mean_cost_usd": 2.0389, + "total_cost_usd": 6.1167, + "contamination_suspect_runs": 0, + "mean_wall_s": 250.6, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 5279550.6667, + "mean_tokens_input": 2628000.6667, + "mean_tokens_output": 23743.6667, + "mean_cost_usd": 0.8906, + "total_cost_usd": 2.6718, + "contamination_suspect_runs": 0, + "mean_wall_s": 249.9, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3734954.3333, + "mean_tokens_input": 1861290.3333, + "mean_tokens_output": 12395.6667, + "mean_cost_usd": 2.2487, + "total_cost_usd": 6.7461, + "contamination_suspect_runs": 0, + "mean_wall_s": 235.2, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.125, + "mean_secondary_score": 0.75, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4202367.5, + "mean_tokens_input": 2093650.5, + "mean_tokens_output": 15224.0, + "mean_cost_usd": 0.6808, + "total_cost_usd": 1.3617, + "contamination_suspect_runs": 0, + "mean_wall_s": 199.95, + "statuses": { + "task_fail": 1, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 1 + }, + "mean_tokens_total": 3244884.0, + "mean_tokens_input": 1616682.0, + "mean_tokens_output": 11646.0, + "mean_cost_usd": 0.615, + "total_cost_usd": 0.615, + "contamination_suspect_runs": 0, + "mean_wall_s": 132.2, + "statuses": { + "ok": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3158432.0, + "mean_tokens_input": 1574377.6667, + "mean_tokens_output": 9697.0, + "mean_cost_usd": 1.5568, + "total_cost_usd": 4.6704, + "contamination_suspect_runs": 0, + "mean_wall_s": 157.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 0.75, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4539845.0, + "mean_tokens_input": 2263375.0, + "mean_tokens_output": 13267.6667, + "mean_cost_usd": 0.7303, + "total_cost_usd": 2.191, + "contamination_suspect_runs": 0, + "mean_wall_s": 223.7, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2242228.3333, + "mean_tokens_input": 1117914.0, + "mean_tokens_output": 6417.3333, + "mean_cost_usd": 1.306, + "total_cost_usd": 3.918, + "contamination_suspect_runs": 0, + "mean_wall_s": 153.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9583, + "mean_secondary_score": 1.0, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 3467477.6667, + "mean_tokens_input": 1729058.3333, + "mean_tokens_output": 10344.3333, + "mean_cost_usd": 0.5968, + "total_cost_usd": 1.7904, + "contamination_suspect_runs": 0, + "mean_wall_s": 132.2667, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 2368378.6667, + "mean_tokens_input": 1181231.6667, + "mean_tokens_output": 5931.0, + "mean_cost_usd": 1.4462, + "total_cost_usd": 4.3385, + "contamination_suspect_runs": 0, + "mean_wall_s": 122.2333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5417, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4584002.6667, + "mean_tokens_input": 2279746.3333, + "mean_tokens_output": 24678.6667, + "mean_cost_usd": 0.7666, + "total_cost_usd": 2.2999, + "contamination_suspect_runs": 0, + "mean_wall_s": 265.1333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.875, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3298667.5, + "mean_tokens_input": 1642369.0, + "mean_tokens_output": 13953.0, + "mean_cost_usd": 1.7015, + "total_cost_usd": 3.403, + "contamination_suspect_runs": 0, + "mean_wall_s": 281.45, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.25, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 6091146.0, + "mean_tokens_input": 3032558.0, + "mean_tokens_output": 27575.3333, + "mean_cost_usd": 0.9306, + "total_cost_usd": 2.7918, + "contamination_suspect_runs": 0, + "mean_wall_s": 270.6667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 4577037.0, + "mean_tokens_input": 2281205.0, + "mean_tokens_output": 14654.5, + "mean_cost_usd": 2.3913, + "total_cost_usd": 4.7826, + "contamination_suspect_runs": 0, + "mean_wall_s": 641.5, + "statuses": { + "ok": 2 + }, + "retries": 0 + } + ], + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 20, + "n_valid": 20, + "n_excluded": 0, + "pass_rate": 0.4, + "mean_primary_score": 0.6359, + "mean_secondary_score": 0.6542, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4071723.15, + "mean_tokens_input": 2028755.05, + "mean_tokens_output": 14370.5, + "mean_cost_usd": 0.6452, + "total_cost_usd": 12.9043, + "contamination_suspect_runs": 0, + "mean_wall_s": 202.985, + "statuses": { + "ok": 8, + "task_fail": 7, + "turn_limit": 5 + }, + "retries": 1 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 30, + "n_valid": 30, + "n_excluded": 0, + "pass_rate": 0.4667, + "mean_primary_score": 0.7549, + "mean_secondary_score": 0.6889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2740276.8333, + "mean_tokens_input": 1366678.2333, + "mean_tokens_output": 6939.9667, + "mean_cost_usd": 1.4242, + "total_cost_usd": 42.7265, + "contamination_suspect_runs": 0, + "mean_wall_s": 163.67, + "statuses": { + "ok": 14, + "task_fail": 11, + "turn_limit": 5 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 24, + "n_valid": 24, + "n_excluded": 0, + "pass_rate": 0.75, + "mean_primary_score": 0.9013, + "mean_secondary_score": 0.9583, + "trigger_rate": 0.9583, + "skills_used": { + "authoring-airflow-dags": 12, + "migrating-to-airflow-3": 5, + "testing-airflow-dags": 6 + }, + "mean_tokens_total": 3596645.75, + "mean_tokens_input": 1792591.0417, + "mean_tokens_output": 11823.6667, + "mean_cost_usd": 0.6316, + "total_cost_usd": 15.1595, + "contamination_suspect_runs": 0, + "mean_wall_s": 156.35, + "statuses": { + "ok": 18, + "task_fail": 5, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 17, + "n_valid": 17, + "n_excluded": 0, + "pass_rate": 0.9412, + "mean_primary_score": 0.9916, + "mean_secondary_score": 0.9608, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 6, + "testing-airflow-dags": 9, + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 2896222.5294, + "mean_tokens_input": 1444182.4706, + "mean_tokens_output": 7875.6471, + "mean_cost_usd": 1.5808, + "total_cost_usd": 26.8739, + "contamination_suspect_runs": 0, + "mean_wall_s": 150.8, + "statuses": { + "ok": 16, + "task_fail": 1 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 10, + "n_valid": 10, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4895, + "mean_secondary_score": 0.7333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3894416.5, + "mean_tokens_input": 1938725.6, + "mean_tokens_output": 17114.4, + "mean_cost_usd": 0.6823, + "total_cost_usd": 6.8228, + "contamination_suspect_runs": 0, + "mean_wall_s": 214.44, + "statuses": { + "turn_limit": 2, + "task_fail": 8 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 19, + "n_valid": 19, + "n_excluded": 0, + "pass_rate": 0.3684, + "mean_primary_score": 0.8039, + "mean_secondary_score": 0.6491, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2862489.7895, + "mean_tokens_input": 1427170.2632, + "mean_tokens_output": 8169.5263, + "mean_cost_usd": 1.798, + "total_cost_usd": 34.1627, + "contamination_suspect_runs": 0, + "mean_wall_s": 188.9263, + "statuses": { + "ok": 7, + "task_fail": 11, + "cost_limit": 1 + }, + "retries": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 13, + "n_valid": 13, + "n_excluded": 0, + "pass_rate": 0.3077, + "mean_primary_score": 0.6758, + "mean_secondary_score": 0.8782, + "trigger_rate": 0.7692, + "skills_used": { + "authoring-airflow-dags": 6, + "migrating-to-airflow-3": 3, + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 4442776.4615, + "mean_tokens_input": 2213373.6923, + "mean_tokens_output": 16498.7692, + "mean_cost_usd": 0.8727, + "total_cost_usd": 11.3457, + "contamination_suspect_runs": 0, + "mean_wall_s": 185.3538, + "statuses": { + "ok": 4, + "task_fail": 4, + "turn_limit": 5 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 10, + "n_valid": 10, + "n_excluded": 0, + "pass_rate": 0.9, + "mean_primary_score": 0.99, + "mean_secondary_score": 0.9333, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 8, + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 3307574.2, + "mean_tokens_input": 1649811.0, + "mean_tokens_output": 7972.4, + "mean_cost_usd": 1.9606, + "total_cost_usd": 19.6064, + "contamination_suspect_runs": 0, + "mean_wall_s": 240.34, + "statuses": { + "ok": 9, + "task_fail": 1 + }, + "retries": 0 + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 9, + "mean_delta_pass_rate": 0.4259, + "ci95_delta_pass_rate": [ + 0.0926, + 0.7778 + ], + "mean_delta_primary_score": 0.2667, + "ci95_delta_primary_score": [ + 0.0299, + 0.5093 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-clickstream-claim-check", + "baseline_pass_rate": 0.5, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.1667, + "delta_primary_score": -0.1191 + }, + { + "case": "migration-inventory-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.4 + }, + { + "case": "migration-revenue-previous-day", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.5714 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.7143 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.2917 + }, + { + "case": "testing-custom-operator-conn", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.875 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.25 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 3, + "mean_delta_pass_rate": 0.1667, + "ci95_delta_pass_rate": [ + 0.0, + 0.5 + ], + "mean_delta_primary_score": 0.0139, + "ci95_delta_primary_score": [ + -0.2917, + 0.2778 + ], + "cases": [ + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.2778 + }, + { + "case": "scheduling-backfill-pageviews", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.5, + "delta_pass_rate": 0.5, + "delta_primary_score": 0.0556 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.2917 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 6, + "mean_delta_pass_rate": 0.2222, + "ci95_delta_pass_rate": [ + 0.0555, + 0.3889 + ], + "mean_delta_primary_score": 0.1389, + "ci95_delta_primary_score": [ + 0.0278, + 0.25 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-manual-trigger-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.5, + "delta_pass_rate": 0.5, + "delta_primary_score": 0.1667 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 0.5, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.5, + "delta_primary_score": 0.3333 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.3333 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 4, + "mean_delta_pass_rate": 0.3333, + "ci95_delta_pass_rate": [ + -0.1666, + 0.8334 + ], + "mean_delta_primary_score": 0.0785, + "ci95_delta_primary_score": [ + -0.0167, + 0.1736 + ], + "cases": [ + { + "case": "authoring-daily-revenue-yesterday", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.2222 + }, + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-nondeterministic-parse", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.0333 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.125 + } + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 10, + "n_pairs": 15, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.3444, + "ci95_delta_pass_rate": [ + 0.1146, + 0.6333 + ], + "mean_delta_primary_score": 0.2155, + "ci95_delta_primary_score": [ + 0.0536, + 0.4125 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 5, + "n_pairs": 7, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2619, + "ci95_delta_pass_rate": [ + -0.037, + 0.5556 + ], + "mean_delta_primary_score": 0.0508, + "ci95_delta_primary_score": [ + -0.0492, + 0.1519 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ], + "failure_classes": { + "ok": 76, + "task_fail": 48, + "turn_limit": 18, + "cost_limit": 1 + }, + "total_cost_usd": 169.6017, + "isolation": { + "contamination_suspects": [], + "kill_command_runs": [], + "agent_env_restored_runs": [], + "agent_env_restore_failed_runs": [] + }, + "sources": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30" + }, + { + "dir": "evals/airflow/results/baseline-run3-2026-09-30" + }, + { + "dir": "evals/airflow/results/skill-2026-09-30" + } + ], + "n_runs": 143 +} \ No newline at end of file diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/REPORT.md b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/REPORT.md new file mode 100644 index 0000000..751009d --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/REPORT.md @@ -0,0 +1,225 @@ +# Airflow skill eval report + +Pass rate = fraction of valid runs whose primary checks all passed. Excluded runs (infra_error, grader_error, skipped_budget) are listed but not counted. + +## dev: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 38 (38) | 34% | 0.655 | 0.695 | 0% | 3.93e+06 | 0.648 | 190 | {'ok': 13, 'task_fail': 18, 'turn_limit': 7} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 37 (37) | 54% | 0.796 | 0.712 | 0% | 2.8e+06 | 1.45 | 169 | {'ok': 20, 'task_fail': 12, 'turn_limit': 5} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 36 (36) | 81% | 0.925 | 0.917 | 92% | 3.79e+06 | 0.642 | 159 | {'ok': 29, 'task_fail': 6, 'turn_limit': 1} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 33 (33) | 94% | 0.991 | 0.929 | 100% | 3.48e+06 | 1.83 | 175 | {'ok': 31, 'task_fail': 2} | + +## dev: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 13 | 0.462 | (0.2051, 0.6923) | 0.266 | (0.0855, 0.4491) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 12 | 0.333 | (0.1111, 0.5833) | 0.182 | (0.0571, 0.3608) | +| pooled (25 case x model pairs) | 13 | 0.4 | (0.2, 0.6061) | 0.226 | (0.1013, 0.3483) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## dev: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 0.667 | - | 0.35 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.722 | {'ok': 3} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.857 | 1 | {'authoring-airflow-dags': 3} | 0.691 | {'ok': 2, 'task_fail': 1} | +| authoring-airflow2-inventory-snapshot | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.31 | {'ok': 3} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.905 | 0.889 | - | 0.924 | {'task_fail': 2, 'ok': 1} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | - | 1.36 | {'ok': 3} | +| authoring-clickstream-claim-check | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.809 | 1 | {'authoring-airflow-dags': 3} | 0.645 | {'ok': 2, 'task_fail': 1} | +| authoring-clickstream-claim-check | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 2.56 | {'ok': 3} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.944 | 0.667 | - | 0.605 | {'ok': 1, 'task_fail': 2} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 0.5 | - | 1.43 | {'ok': 2} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.861 | 0.778 | {'authoring-airflow-dags': 1} | 0.704 | {'ok': 1, 'task_fail': 2} | +| authoring-vendor-shipments-etl | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | {'authoring-airflow-dags': 3} | 2.29 | {'ok': 3} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.714 | 0.667 | - | 0.392 | {'task_fail': 3} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 0.816 | {'task_fail': 3} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.905 | 0.889 | {'migrating-to-airflow-3': 3} | 0.607 | {'task_fail': 1, 'ok': 2} | +| debugging-manual-trigger-context | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.905 | 0.778 | {'migrating-to-airflow-3': 3} | 1.52 | {'task_fail': 2, 'ok': 1} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 0.833 | - | 0.444 | {'ok': 2} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0 | 0.333 | - | 1 | {'task_fail': 3} | +| debugging-variable-at-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 1 | {'migrating-to-airflow-3': 2} | 0.375 | {'ok': 2} | +| debugging-variable-at-parse | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 2} | 0.82 | {'ok': 2} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.533 | 0.222 | - | 0.572 | {'task_fail': 3} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.8 | 0.444 | - | 1.48 | {'task_fail': 3} | +| migration-inventory-context | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 3} | 0.699 | {'ok': 3} | +| migration-inventory-context | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'migrating-to-airflow-3': 2} | 2.23 | {'ok': 2} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.476 | 0.667 | - | 0.755 | {'turn_limit': 3} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.762 | 0.667 | - | 2.41 | {'turn_limit': 2, 'task_fail': 1} | +| migration-revenue-previous-day | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 0.667 | {'migrating-to-airflow-3': 3} | 0.682 | {'ok': 3} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.333 | 0.333 | - | 0.827 | {'task_fail': 2, 'turn_limit': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.714 | 0.778 | - | 2.74 | {'turn_limit': 3} | +| migration-weekly-catchup | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'migrating-to-airflow-3': 3} | 0.728 | {'ok': 3} | +| migration-weekly-catchup | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | {'migrating-to-airflow-3': 2} | 2.65 | {'ok': 2} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.611 | 0.889 | - | 0.778 | {'task_fail': 2, 'ok': 1} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.778 | 0.778 | - | 0.993 | {'task_fail': 1, 'ok': 2} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 1 | {'authoring-airflow-dags': 2} | 0.602 | {'ok': 2} | +| scheduling-idempotent-orders-load | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.83 | {'ok': 3} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.429 | 0.667 | - | 0.505 | {'task_fail': 2, 'ok': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.667 | 0.667 | - | 0.844 | {'ok': 2, 'task_fail': 1} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 0.37 | {'ok': 3} | +| scheduling-weekday-ny-business-day | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.58 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 100% | 1 | 1 | - | 0.713 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 2.04 | {'ok': 3} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 1 | {'testing-airflow-dags': 3} | 0.891 | {'turn_limit': 1, 'ok': 2} | +| testing-airflow2-ci-suite | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 2.25 | {'ok': 3} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.125 | 0.833 | - | 0.762 | {'turn_limit': 2, 'task_fail': 1} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 100% | 1 | 1 | - | 1.74 | {'ok': 2} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 100% | 1 | 1 | {'testing-airflow-dags': 2} | 0.663 | {'ok': 2} | +| testing-custom-operator-conn | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.56 | {'ok': 3} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.708 | 0.75 | - | 0.73 | {'turn_limit': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | - | 1.31 | {'ok': 3} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.958 | 1 | {'testing-airflow-dags': 2} | 0.597 | {'task_fail': 1, 'ok': 2} | +| testing-dagbag-integrity | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 1.45 | {'ok': 3} | + +## holdout: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 20 (20) | 20% | 0.552 | 0.717 | 0% | 3.8e+06 | 0.676 | 200 | {'ok': 4, 'turn_limit': 2, 'task_fail': 14} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 20 (20) | 35% | 0.792 | 0.65 | 0% | 2.94e+06 | 1.83 | 194 | {'ok': 7, 'task_fail': 12, 'cost_limit': 1} | +| skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 19 (19) | 26% | 0.679 | 0.829 | 79% | 4.47e+06 | 0.826 | 185 | {'ok': 5, 'task_fail': 8, 'turn_limit': 6} | +| skill | google-vertex-anthropic/claude-sonnet-4-6@default | 18 (18) | 78% | 0.971 | 0.963 | 100% | 3.98e+06 | 2.47 | 245 | {'ok': 14, 'turn_limit': 1, 'task_fail': 3} | + +## holdout: paired delta (skill - baseline) over cases + +| model | cases | delta pass rate | 95% CI | delta primary score | 95% CI | +|---|---|---|---|---|---| +| google-vertex-anthropic/claude-haiku-4-5@20251001 | 7 | 0.0476 | (0.0, 0.1428) | 0.0868 | (-0.1295, 0.3647) | +| google-vertex-anthropic/claude-sonnet-4-6@default | 6 | 0.389 | (0.0, 0.7778) | 0.137 | (0.0412, 0.2275) | +| pooled (13 case x model pairs) | 7 | 0.205 | (0.0, 0.4286) | 0.11 | (-0.0266, 0.2822) | + +Pooled CI: bootstrap over cases; a resampled case carries the deltas of all its models. + +## holdout: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.889 | 0.889 | - | 0.814 | {'ok': 2, 'task_fail': 1} | +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 33% | 0.778 | 0.778 | - | 2.54 | {'ok': 1, 'task_fail': 1, 'cost_limit': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.833 | 0.889 | {'authoring-airflow-dags': 2} | 1 | {'ok': 2, 'task_fail': 1} | +| authoring-daily-revenue-yesterday | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 3.66 | {'ok': 3} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.444 | 0.889 | - | 0.816 | {'turn_limit': 2, 'task_fail': 1} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.556 | - | 1.79 | {'ok': 3} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.667 | 0.889 | {'authoring-airflow-dags': 2} | 1.14 | {'task_fail': 1, 'turn_limit': 2} | +| authoring-partner-feeds-mapping | authoring | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.778 | {'authoring-airflow-dags': 3} | 3.21 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.967 | 0.778 | - | 0.711 | {'ok': 2, 'task_fail': 1} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 0.667 | - | 0.863 | {'ok': 3} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 67% | 0.967 | 1 | {'authoring-airflow-dags': 1, 'migrating-to-airflow-3': 3} | 0.525 | {'ok': 2, 'task_fail': 1} | +| debugging-nondeterministic-parse | debugging | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 67% | 0.967 | 1 | {'authoring-airflow-dags': 3} | 0.863 | {'ok': 2, 'task_fail': 1} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.429 | 0.222 | - | 0.662 | {'task_fail': 3} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.571 | 0.444 | - | 2.14 | {'task_fail': 3} | +| migration-hourly-pageviews-templates | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 1 (1) | 0% | 0.286 | 0.667 | {'migrating-to-airflow-3': 1} | 0.819 | {'task_fail': 1} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0 | 0.556 | - | 0.474 | {'task_fail': 3} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.571 | 0.556 | - | 2.58 | {'task_fail': 3} | +| migration-shipments-assets | migration | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.857 | 0.444 | {'migrating-to-airflow-3': 3} | 0.809 | {'task_fail': 2, 'turn_limit': 1} | +| migration-shipments-assets | migration | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.857 | 1 | {'migrating-to-airflow-3': 3} | 2.62 | {'turn_limit': 1, 'task_fail': 2} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 2 (2) | 0% | 0.611 | 0.667 | - | 0.395 | {'task_fail': 2} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 0% | 0.778 | 0.667 | - | 1.12 | {'task_fail': 3} | +| scheduling-backfill-pageviews | scheduling | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 33% | 0.63 | 0.889 | {'authoring-airflow-dags': 2} | 0.555 | {'task_fail': 2, 'ok': 1} | +| scheduling-backfill-pageviews | scheduling | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'authoring-airflow-dags': 3} | 1.88 | {'ok': 3} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.542 | 1 | - | 0.767 | {'task_fail': 3} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 2 (2) | 0% | 0.875 | 1 | - | 1.7 | {'task_fail': 2} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (3) | 0% | 0.25 | 0.917 | {'testing-airflow-dags': 2} | 0.931 | {'turn_limit': 3} | +| testing-taskflow-transforms | testing | skill | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (3) | 100% | 1 | 1 | {'testing-airflow-dags': 3} | 2.6 | {'ok': 3} | + +## Failure classes (all runs) + +{'ok': 123, 'task_fail': 75, 'turn_limit': 22, 'cost_limit': 1} + +Total spend (all attempts, all dirs): $271.86 + +## Isolation + +Contamination suspects: 78 run(s). +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: read:/baseline-2026-09-30-baseline-20260930-yesterday-pyn34e58/ws/README.md (work-root), bash:/first_run.csv (shared-temp), bash:/second_run.csv (shared-temp), bash:/first_run.csv (shared-temp), bash:/second_run.csv (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp), bash:/partner_feed_before.py (shared-temp), bash:/partner_feed_after.py (shared-temp), bash:/partner_feed_before.py (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow-test- (shared-temp), bash:////airflow-test- (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/vendor_shipments_verification.md (shared-temp), bash:/vendor_shipments_verification.md (shared-temp) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp) +- debugging-variable-at-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/warehouse (shared-temp), bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- migration-hourly-pageviews-templates baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp), bash:/detailed_changes.md (shared-temp), bash:/detailed_changes.md (shared-temp) +- migration-inventory-context baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/inv_test (shared-temp) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp) +- migration-revenue-previous-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: read:/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/dags/sql/daily_revenue.sql (work-root), read:/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/data/orders.csv (work-root), bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp), bash:/MIGRATION_CHECKLIST.md (shared-temp) +- migration-weekly-catchup baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_summary.md (shared-temp), bash:/migration_summary.md (shared-temp) +- scheduling-weekday-ny-business-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/CHANGES_SUMMARY.md (shared-temp), bash:/CHANGES_SUMMARY.md (shared-temp), bash:/SCHEDULE_DIAGRAM.txt (shared-temp), bash:/SCHEDULE_DIAGRAM.txt (shared-temp) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp), bash:/CHANGES.txt (shared-temp), bash:/CHANGES.txt (shared-temp) +- testing-airflow2-ci-suite baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/analyze_shipments.py (shared-temp), bash:/analyze_shipments.py (shared-temp), bash:/test_summary.txt (shared-temp), bash:/test_summary.txt (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/test.db (shared-temp), bash:/test.db (shared-temp), bash:/test.db (shared-temp), bash:/testdb.duckdb (shared-temp), bash:/testdb.duckdb (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: write:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py (work-root), bash:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py (work-root) +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/summary.txt (shared-temp), bash:/summary.txt (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/landing (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp) +- debugging-variable-at-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: read:/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/debugging-variable-at-parse-y19jdpne/ws/dags/warehouse_load.py (work-root) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/before_after.txt (shared-temp), bash:/before_after.txt (shared-temp) +- migration-hourly-pageviews-templates baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash: (shared-temp) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp), bash:/verification.txt (shared-temp), bash:/verification.txt (shared-temp) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/airflow-test- (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp), bash:/airflow-test-idempotent (shared-temp) +- testing-custom-operator-conn baseline google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/test.duckdb (shared-temp) +- authoring-clickstream-claim-check baseline google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/airflow_test_ (shared-temp), bash:/airflow_test2 (shared-temp), bash:/airflow_test2 (shared-temp), bash:/airflow_test2 (shared-temp) +- authoring-clickstream-claim-check baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/dag_summary.txt (shared-temp), bash:/dag_summary.txt (shared-temp) +- authoring-daily-revenue-yesterday baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/authoring-daily-revenue-yesterday-ew4q6g1i/ws (work-root) +- authoring-vendor-shipments-etl baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp), bash:/airflow_test (shared-temp) +- debugging-manual-trigger-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: read:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/debugging-manual-trigger-context-1x63tzof/ws/dags/partner_feed.py (work-root) +- debugging-nondeterministic-parse baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/fix_summary.md (shared-temp), bash:/fix_summary.md (shared-temp) +- migration-inventory-context baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/upgrade_summary.md (shared-temp), bash:/upgrade_summary.md (shared-temp), bash:/baseline-run3-2026-09-30-baseline-20260930-182417/migration-inventory-context-im02tn8a/ws/dags/inventory_snapshot.py (work-root) +- migration-shipments-assets baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: read:/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/migration-shipments-assets-bz1624jc/ws/dags/carrier_scorecard.py (work-root) +- scheduling-idempotent-orders-load baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/dag_fix_summary.md (shared-temp), bash:/dag_fix_summary.md (shared-temp) +- scheduling-weekday-ny-business-day baseline google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/fx_pnl_changes_summary.md (shared-temp), bash:/fx_pnl_changes_summary.md (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_csp (shared-temp), bash:/airflow_test_csp (shared-temp), bash:/airflow_test_csp (shared-temp) +- authoring-daily-revenue-yesterday skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_ (shared-temp), bash:/airflow_test_dr (shared-temp), bash:/airflow_test_dr (shared-temp), bash:/airflow_test_dr2 (shared-temp), bash:/airflow_test_dr2 (shared-temp) +- debugging-manual-trigger-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp), bash:/deployment_guide.md (shared-temp), bash:/deployment_guide.md (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:////airflow_vendor_test_ (shared-temp), bash:/airflow_vendor_home_ (shared-temp), bash:////airflow_vendor_test2_ (shared-temp), bash:/airflow_vendor_home2_ (shared-temp), bash:////airflow_vendor_test2_ (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/test_warehouse (shared-temp) +- migration-hourly-pageviews-templates skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/migration_notes.md (shared-temp), bash:/migration_notes.md (shared-temp), bash:/before_baseline.txt (shared-temp), bash:/before_baseline.txt (shared-temp), bash:/after_verification.txt (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/airflow_test_pv_ (shared-temp), bash:/airflow_test_pv_ (shared-temp), bash:/airflow_test_pv2 (shared-temp), bash:/airflow_test_pv2 (shared-temp), bash:/airflow_test_pv2 (shared-temp) +- scheduling-weekday-ny-business-day skill google-vertex-anthropic/claude-sonnet-4-6@default run 1: bash:/af_test_ (shared-temp) +- testing-custom-operator-conn skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 1: bash:/test_airflow (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_1 (shared-temp), bash:/af_test_ (shared-temp), bash:/first_run_2026-09-27.csv (shared-temp) +- authoring-partner-feeds-mapping skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/test_dag.py (shared-temp), bash:/test_dag.py (shared-temp), bash:/verify_dag_structure.py (shared-temp), bash:/verify_dag_structure.py (shared-temp), bash:/verify_dag_structure.py (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:.nymKSGkNb1 (shared-temp), bash:/ (shared-temp) +- debugging-manual-trigger-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/test_summary.md (shared-temp), bash:/test_summary.md (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp) +- debugging-nondeterministic-parse skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/warehouse_ingest_fix_summary.md (shared-temp), bash:/warehouse_ingest_fix_summary.md (shared-temp) +- authoring-vendor-shipments-etl skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/vendor_shipments_summary.md (shared-temp), bash:/vendor_shipments_summary.md (shared-temp), bash:/requirements_checklist.txt (shared-temp), bash:/requirements_checklist.txt (shared-temp), bash:/usage_guide.md (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/after.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/after.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/af_test_pv_ (shared-temp), bash:/af_test_pv_ (shared-temp), bash:/af_test_pv_ (shared-temp), bash:/af_test_pv2 (shared-temp), bash:/af_test_pv2 (shared-temp) +- scheduling-idempotent-orders-load skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp), bash:/test_dag.sh (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 2: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- authoring-airflow2-inventory-snapshot skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/airflow_test_ (shared-temp), bash:/airflow_test_ (shared-temp), bash:/airflow_test_inv (shared-temp), bash:/airflow_inv2 (shared-temp), bash:/airflow_inv2 (shared-temp) +- authoring-clickstream-claim-check skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/first_run_2026-09-29.csv (shared-temp), bash:/first_run_2026-09-29.csv (shared-temp) +- testing-taskflow-transforms skill google-vertex-anthropic/claude-sonnet-4-6@default run 2: bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp), bash:/orders_enrichment.py.bak (shared-temp) +- debugging-variable-at-parse skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp) +- migration-inventory-context skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-revenue-previous-day skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/migration_summary.txt (shared-temp), bash:/migration_summary.txt (shared-temp) +- migration-shipments-assets skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/before.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- migration-weekly-catchup skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/before.json (shared-temp), bash:/after.json (shared-temp), bash:/before.json (shared-temp), bash:/after.json (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:/af_test_ (shared-temp), bash:/af_test_ (shared-temp), bash:/af_test2_ (shared-temp), bash:/af_test2_ (shared-temp), bash:/af_test2_ (shared-temp) +- scheduling-backfill-pageviews skill google-vertex-anthropic/claude-haiku-4-5@20251001 run 3: bash:/test_pageviews_backfill.py (shared-temp), bash:/test_pageviews_backfill.py (shared-temp), bash:/BACKFILL_SETUP_SUMMARY.md (shared-temp), bash:/BACKFILL_SETUP_SUMMARY.md (shared-temp), bash:/test_idempotency.py (shared-temp) +- scheduling-idempotent-orders-load skill google-vertex-anthropic/claude-sonnet-4-6@default run 3: bash:.IKuqtA2Qn0 (shared-temp), bash:.IKuqtA2Qn0 (shared-temp), bash:.IKuqtA2Qn0 (shared-temp) +Broad kill commands (pkill/killall/kill by pattern): 0 run(s). None. +Agent env restored after: 0 run(s); restore failed: 0. + +## Sources + +- `evals/airflow/results/baseline-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/baseline-run3-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None +- `evals/airflow/results/skill-2026-09-30`: arm=None models=None altimate-code=None started=None budget=None diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/results.json b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/results.json new file mode 100644 index 0000000..4e102bf --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/excluding-strong/results.json @@ -0,0 +1,5246 @@ +{ + "groups": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2181044.0, + "mean_tokens_input": 1087424.6667, + "mean_tokens_output": 6285.0, + "mean_cost_usd": 0.3499, + "total_cost_usd": 1.0498, + "contamination_suspect_runs": 0, + "mean_wall_s": 92.4, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1174723.0, + "mean_tokens_input": 585698.3333, + "mean_tokens_output": 3335.3333, + "mean_cost_usd": 0.7217, + "total_cost_usd": 2.1651, + "contamination_suspect_runs": 0, + "mean_wall_s": 84.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8571, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3917650.0, + "mean_tokens_input": 1953553.3333, + "mean_tokens_output": 10690.6667, + "mean_cost_usd": 0.6908, + "total_cost_usd": 2.0723, + "contamination_suspect_runs": 0, + "mean_wall_s": 171.7, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 2666065.0, + "mean_tokens_input": 1330012.6667, + "mean_tokens_output": 6056.3333, + "mean_cost_usd": 1.311, + "total_cost_usd": 3.933, + "contamination_suspect_runs": 1, + "mean_wall_s": 116.8667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3346429.3333, + "mean_tokens_input": 1667308.3333, + "mean_tokens_output": 11945.6667, + "mean_cost_usd": 0.9238, + "total_cost_usd": 2.7713, + "contamination_suspect_runs": 1, + "mean_wall_s": 193.0, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 1 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2497066.3333, + "mean_tokens_input": 1245344.0, + "mean_tokens_output": 6397.0, + "mean_cost_usd": 1.3607, + "total_cost_usd": 4.082, + "contamination_suspect_runs": 1, + "mean_wall_s": 150.0, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8095, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3115526.3333, + "mean_tokens_input": 1553145.0, + "mean_tokens_output": 9355.0, + "mean_cost_usd": 0.6453, + "total_cost_usd": 1.9359, + "contamination_suspect_runs": 0, + "mean_wall_s": 126.3333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3810044.0, + "mean_tokens_input": 1900734.3333, + "mean_tokens_output": 8598.3333, + "mean_cost_usd": 2.5605, + "total_cost_usd": 7.6816, + "contamination_suspect_runs": 3, + "mean_wall_s": 180.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8889, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4008946.3333, + "mean_tokens_input": 1999834.6667, + "mean_tokens_output": 9435.0, + "mean_cost_usd": 0.814, + "total_cost_usd": 2.4421, + "contamination_suspect_runs": 3, + "mean_wall_s": 192.9667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2547843.3333, + "mean_tokens_input": 1270822.0, + "mean_tokens_output": 6218.0, + "mean_cost_usd": 2.5449, + "total_cost_usd": 7.6347, + "contamination_suspect_runs": 0, + "mean_wall_s": 165.7333, + "statuses": { + "ok": 1, + "task_fail": 1, + "cost_limit": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.8333, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3804984.3333, + "mean_tokens_input": 1896807.6667, + "mean_tokens_output": 11513.6667, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0007, + "contamination_suspect_runs": 0, + "mean_wall_s": 151.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 2919397.0, + "mean_tokens_input": 1456704.3333, + "mean_tokens_output": 6007.0, + "mean_cost_usd": 3.6571, + "total_cost_usd": 10.9713, + "contamination_suspect_runs": 1, + "mean_wall_s": 160.3333, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4444, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4851110.6667, + "mean_tokens_input": 2417172.0, + "mean_tokens_output": 16957.6667, + "mean_cost_usd": 0.8155, + "total_cost_usd": 2.4465, + "contamination_suspect_runs": 0, + "mean_wall_s": 245.9333, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2201060.6667, + "mean_tokens_input": 1097202.0, + "mean_tokens_output": 6673.3333, + "mean_cost_usd": 1.7917, + "total_cost_usd": 5.3752, + "contamination_suspect_runs": 0, + "mean_wall_s": 177.4333, + "statuses": { + "ok": 3 + }, + "retries": 1 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 5343871.3333, + "mean_tokens_input": 2660900.6667, + "mean_tokens_output": 22267.0, + "mean_cost_usd": 1.136, + "total_cost_usd": 3.4079, + "contamination_suspect_runs": 1, + "mean_wall_s": 241.8667, + "statuses": { + "task_fail": 1, + "turn_limit": 2 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 4314617.0, + "mean_tokens_input": 2152584.6667, + "mean_tokens_output": 9472.3333, + "mean_cost_usd": 3.2137, + "total_cost_usd": 9.6411, + "contamination_suspect_runs": 0, + "mean_wall_s": 215.1667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9444, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4774489.6667, + "mean_tokens_input": 2379971.3333, + "mean_tokens_output": 14735.0, + "mean_cost_usd": 0.6048, + "total_cost_usd": 1.8143, + "contamination_suspect_runs": 3, + "mean_wall_s": 211.1667, + "statuses": { + "ok": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3092601.0, + "mean_tokens_input": 1543025.5, + "mean_tokens_output": 6572.5, + "mean_cost_usd": 1.431, + "total_cost_usd": 2.8619, + "contamination_suspect_runs": 2, + "mean_wall_s": 184.15, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.8611, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.3333, + "skills_used": { + "authoring-airflow-dags": 1 + }, + "mean_tokens_total": 4055158.0, + "mean_tokens_input": 2021327.0, + "mean_tokens_output": 13611.0, + "mean_cost_usd": 0.704, + "total_cost_usd": 2.112, + "contamination_suspect_runs": 2, + "mean_wall_s": 235.8, + "statuses": { + "ok": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3231194.0, + "mean_tokens_input": 1611493.0, + "mean_tokens_output": 8227.3333, + "mean_cost_usd": 2.2869, + "total_cost_usd": 6.8606, + "contamination_suspect_runs": 2, + "mean_wall_s": 263.2, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1987527.6667, + "mean_tokens_input": 991131.6667, + "mean_tokens_output": 5340.6667, + "mean_cost_usd": 0.3924, + "total_cost_usd": 1.1771, + "contamination_suspect_runs": 3, + "mean_wall_s": 81.7, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1367355.6667, + "mean_tokens_input": 681477.0, + "mean_tokens_output": 4412.6667, + "mean_cost_usd": 0.816, + "total_cost_usd": 2.448, + "contamination_suspect_runs": 0, + "mean_wall_s": 100.6, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.8889, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 3987580.0, + "mean_tokens_input": 1988030.3333, + "mean_tokens_output": 11666.6667, + "mean_cost_usd": 0.6065, + "total_cost_usd": 1.8196, + "contamination_suspect_runs": 1, + "mean_wall_s": 143.6333, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.9048, + "mean_secondary_score": 0.7778, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 3146989.0, + "mean_tokens_input": 1569931.3333, + "mean_tokens_output": 7145.3333, + "mean_cost_usd": 1.5151, + "total_cost_usd": 4.5454, + "contamination_suspect_runs": 1, + "mean_wall_s": 151.6, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3316231.0, + "mean_tokens_input": 1652526.6667, + "mean_tokens_output": 11305.3333, + "mean_cost_usd": 0.7105, + "total_cost_usd": 2.1316, + "contamination_suspect_runs": 3, + "mean_wall_s": 168.1667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171259.0, + "mean_tokens_input": 583511.6667, + "mean_tokens_output": 4245.3333, + "mean_cost_usd": 0.8629, + "total_cost_usd": 2.5888, + "contamination_suspect_runs": 0, + "mean_wall_s": 99.0667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 1, + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 2904388.3333, + "mean_tokens_input": 1447815.6667, + "mean_tokens_output": 8862.3333, + "mean_cost_usd": 0.5251, + "total_cost_usd": 1.5754, + "contamination_suspect_runs": 1, + "mean_wall_s": 116.9333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9667, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1912694.3333, + "mean_tokens_input": 954439.6667, + "mean_tokens_output": 3828.0, + "mean_cost_usd": 0.8633, + "total_cost_usd": 2.5899, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.4667, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2600769.0, + "mean_tokens_input": 1297275.0, + "mean_tokens_output": 6320.5, + "mean_cost_usd": 0.4441, + "total_cost_usd": 0.8881, + "contamination_suspect_runs": 2, + "mean_wall_s": 140.8, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1248596.6667, + "mean_tokens_input": 622768.3333, + "mean_tokens_output": 3070.3333, + "mean_cost_usd": 1.0002, + "total_cost_usd": 3.0005, + "contamination_suspect_runs": 0, + "mean_wall_s": 80.3667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 1318139.5, + "mean_tokens_input": 657368.5, + "mean_tokens_output": 3448.0, + "mean_cost_usd": 0.3753, + "total_cost_usd": 0.7506, + "contamination_suspect_runs": 1, + "mean_wall_s": 46.1, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 1644664.0, + "mean_tokens_input": 820097.5, + "mean_tokens_output": 4481.0, + "mean_cost_usd": 0.8201, + "total_cost_usd": 1.6401, + "contamination_suspect_runs": 2, + "mean_wall_s": 87.2, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4286, + "mean_secondary_score": 0.2222, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3891013.6667, + "mean_tokens_input": 1938064.6667, + "mean_tokens_output": 15034.0, + "mean_cost_usd": 0.6625, + "total_cost_usd": 1.9874, + "contamination_suspect_runs": 1, + "mean_wall_s": 200.9667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.4444, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3999267.3333, + "mean_tokens_input": 1994081.6667, + "mean_tokens_output": 11132.0, + "mean_cost_usd": 2.1353, + "total_cost_usd": 6.406, + "contamination_suspect_runs": 1, + "mean_wall_s": 246.5, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 1, + "n_valid": 1, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.2857, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 1 + }, + "mean_tokens_total": 4635817.0, + "mean_tokens_input": 2312903.0, + "mean_tokens_output": 10183.0, + "mean_cost_usd": 0.8192, + "total_cost_usd": 0.8192, + "contamination_suspect_runs": 1, + "mean_wall_s": 130.3, + "statuses": { + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5333, + "mean_secondary_score": 0.2222, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3253703.6667, + "mean_tokens_input": 1620560.0, + "mean_tokens_output": 12716.0, + "mean_cost_usd": 0.5715, + "total_cost_usd": 1.7144, + "contamination_suspect_runs": 1, + "mean_wall_s": 162.7, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8, + "mean_secondary_score": 0.4444, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3057943.3333, + "mean_tokens_input": 1525473.3333, + "mean_tokens_output": 7018.3333, + "mean_cost_usd": 1.4796, + "total_cost_usd": 4.4388, + "contamination_suspect_runs": 1, + "mean_wall_s": 160.7667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 4281023.0, + "mean_tokens_input": 2134509.3333, + "mean_tokens_output": 12163.3333, + "mean_cost_usd": 0.6986, + "total_cost_usd": 2.0959, + "contamination_suspect_runs": 2, + "mean_wall_s": 151.6333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 6511381.5, + "mean_tokens_input": 3248997.0, + "mean_tokens_output": 13427.0, + "mean_cost_usd": 2.2284, + "total_cost_usd": 4.4569, + "contamination_suspect_runs": 2, + "mean_wall_s": 252.5, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.4762, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5878320.3333, + "mean_tokens_input": 2930642.0, + "mean_tokens_output": 17265.6667, + "mean_cost_usd": 0.7551, + "total_cost_usd": 2.2652, + "contamination_suspect_runs": 1, + "mean_wall_s": 267.4333, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7619, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104477.0, + "mean_tokens_input": 3046252.0, + "mean_tokens_output": 12012.6667, + "mean_cost_usd": 2.413, + "total_cost_usd": 7.2389, + "contamination_suspect_runs": 0, + "mean_wall_s": 292.0667, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 5265089.3333, + "mean_tokens_input": 2626082.0, + "mean_tokens_output": 13120.3333, + "mean_cost_usd": 0.6816, + "total_cost_usd": 2.0447, + "contamination_suspect_runs": 2, + "mean_wall_s": 151.6667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3382552.0, + "mean_tokens_input": 1684993.3333, + "mean_tokens_output": 12698.3333, + "mean_cost_usd": 0.4741, + "total_cost_usd": 1.4222, + "contamination_suspect_runs": 3, + "mean_wall_s": 171.5667, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.5556, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5673125.6667, + "mean_tokens_input": 2829871.3333, + "mean_tokens_output": 13420.0, + "mean_cost_usd": 2.5848, + "total_cost_usd": 7.7545, + "contamination_suspect_runs": 0, + "mean_wall_s": 307.5333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8571, + "mean_secondary_score": 0.4444, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 5589411.6667, + "mean_tokens_input": 2786729.6667, + "mean_tokens_output": 16157.3333, + "mean_cost_usd": 0.809, + "total_cost_usd": 2.4269, + "contamination_suspect_runs": 2, + "mean_wall_s": 199.7333, + "statuses": { + "task_fail": 2, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.8571, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 6553546.0, + "mean_tokens_input": 3270860.6667, + "mean_tokens_output": 11864.0, + "mean_cost_usd": 2.6164, + "total_cost_usd": 7.8493, + "contamination_suspect_runs": 3, + "mean_wall_s": 233.7667, + "statuses": { + "turn_limit": 1, + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.3333, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5389009.0, + "mean_tokens_input": 2686409.3333, + "mean_tokens_output": 16401.3333, + "mean_cost_usd": 0.8271, + "total_cost_usd": 2.4814, + "contamination_suspect_runs": 1, + "mean_wall_s": 249.8333, + "statuses": { + "task_fail": 2, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6104354.6667, + "mean_tokens_input": 3046435.3333, + "mean_tokens_output": 11525.0, + "mean_cost_usd": 2.7417, + "total_cost_usd": 8.225, + "contamination_suspect_runs": 0, + "mean_wall_s": 316.8667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 3 + }, + "mean_tokens_total": 4453910.3333, + "mean_tokens_input": 2221128.3333, + "mean_tokens_output": 11818.3333, + "mean_cost_usd": 0.7281, + "total_cost_usd": 2.1844, + "contamination_suspect_runs": 3, + "mean_wall_s": 237.1333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "migrating-to-airflow-3": 2 + }, + "mean_tokens_total": 6052756.5, + "mean_tokens_input": 3020207.5, + "mean_tokens_output": 12378.0, + "mean_cost_usd": 2.6509, + "total_cost_usd": 5.3017, + "contamination_suspect_runs": 2, + "mean_wall_s": 239.5, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.6111, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1990823.0, + "mean_tokens_input": 991262.0, + "mean_tokens_output": 8376.0, + "mean_cost_usd": 0.3948, + "total_cost_usd": 0.7895, + "contamination_suspect_runs": 0, + "mean_wall_s": 129.85, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1818863.6667, + "mean_tokens_input": 907147.6667, + "mean_tokens_output": 4581.3333, + "mean_cost_usd": 1.118, + "total_cost_usd": 3.3541, + "contamination_suspect_runs": 0, + "mean_wall_s": 107.8333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.6296, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.6667, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 3026350.6667, + "mean_tokens_input": 1506925.3333, + "mean_tokens_output": 12615.0, + "mean_cost_usd": 0.5554, + "total_cost_usd": 1.6661, + "contamination_suspect_runs": 1, + "mean_wall_s": 144.9333, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3569779.3333, + "mean_tokens_input": 1780953.3333, + "mean_tokens_output": 7895.0, + "mean_cost_usd": 1.8833, + "total_cost_usd": 5.6498, + "contamination_suspect_runs": 3, + "mean_wall_s": 160.3, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.6111, + "mean_secondary_score": 0.8889, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3885827.0, + "mean_tokens_input": 1933925.3333, + "mean_tokens_output": 18127.0, + "mean_cost_usd": 0.7777, + "total_cost_usd": 2.333, + "contamination_suspect_runs": 2, + "mean_wall_s": 225.1667, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7778, + "mean_secondary_score": 0.7778, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1743515.6667, + "mean_tokens_input": 869117.3333, + "mean_tokens_output": 5294.3333, + "mean_cost_usd": 0.9925, + "total_cost_usd": 2.9774, + "contamination_suspect_runs": 1, + "mean_wall_s": 134.0333, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 2 + }, + "mean_tokens_total": 4200411.0, + "mean_tokens_input": 2094730.5, + "mean_tokens_output": 11109.5, + "mean_cost_usd": 0.6018, + "total_cost_usd": 1.2036, + "contamination_suspect_runs": 0, + "mean_wall_s": 142.4, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3641256.3333, + "mean_tokens_input": 1816170.0, + "mean_tokens_output": 8938.6667, + "mean_cost_usd": 1.8291, + "total_cost_usd": 5.4873, + "contamination_suspect_runs": 2, + "mean_wall_s": 171.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.3333, + "mean_primary_score": 0.4286, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3275582.0, + "mean_tokens_input": 1632436.0, + "mean_tokens_output": 10835.0, + "mean_cost_usd": 0.505, + "total_cost_usd": 1.5151, + "contamination_suspect_runs": 2, + "mean_wall_s": 153.5, + "statuses": { + "task_fail": 2, + "ok": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1171232.0, + "mean_tokens_input": 583887.0, + "mean_tokens_output": 3467.6667, + "mean_cost_usd": 0.8437, + "total_cost_usd": 2.531, + "contamination_suspect_runs": 0, + "mean_wall_s": 83.7333, + "statuses": { + "ok": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 1716857.3333, + "mean_tokens_input": 855214.0, + "mean_tokens_output": 6491.3333, + "mean_cost_usd": 0.3704, + "total_cost_usd": 1.1112, + "contamination_suspect_runs": 0, + "mean_wall_s": 86.9, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 3 + }, + "mean_tokens_total": 3006424.6667, + "mean_tokens_input": 1499916.3333, + "mean_tokens_output": 6611.0, + "mean_cost_usd": 1.5832, + "total_cost_usd": 4.7496, + "contamination_suspect_runs": 1, + "mean_wall_s": 141.0, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4750311.6667, + "mean_tokens_input": 2366086.6667, + "mean_tokens_output": 18311.6667, + "mean_cost_usd": 0.7134, + "total_cost_usd": 2.1401, + "contamination_suspect_runs": 1, + "mean_wall_s": 226.3667, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3606247.0, + "mean_tokens_input": 1796701.0, + "mean_tokens_output": 12870.0, + "mean_cost_usd": 2.0389, + "total_cost_usd": 6.1167, + "contamination_suspect_runs": 0, + "mean_wall_s": 250.6, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 5279550.6667, + "mean_tokens_input": 2628000.6667, + "mean_tokens_output": 23743.6667, + "mean_cost_usd": 0.8906, + "total_cost_usd": 2.6718, + "contamination_suspect_runs": 0, + "mean_wall_s": 249.9, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3734954.3333, + "mean_tokens_input": 1861290.3333, + "mean_tokens_output": 12395.6667, + "mean_cost_usd": 2.2487, + "total_cost_usd": 6.7461, + "contamination_suspect_runs": 0, + "mean_wall_s": 235.2, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.125, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4753905.3333, + "mean_tokens_input": 2369198.0, + "mean_tokens_output": 15689.3333, + "mean_cost_usd": 0.762, + "total_cost_usd": 2.286, + "contamination_suspect_runs": 1, + "mean_wall_s": 223.8333, + "statuses": { + "turn_limit": 2, + "task_fail": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3199189.5, + "mean_tokens_input": 1594765.5, + "mean_tokens_output": 9682.0, + "mean_cost_usd": 1.7393, + "total_cost_usd": 3.4786, + "contamination_suspect_runs": 2, + "mean_wall_s": 240.45, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 3386412.5, + "mean_tokens_input": 1687008.0, + "mean_tokens_output": 12525.0, + "mean_cost_usd": 0.6626, + "total_cost_usd": 1.3252, + "contamination_suspect_runs": 1, + "mean_wall_s": 142.25, + "statuses": { + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3158432.0, + "mean_tokens_input": 1574377.6667, + "mean_tokens_output": 9697.0, + "mean_cost_usd": 1.5568, + "total_cost_usd": 4.6704, + "contamination_suspect_runs": 0, + "mean_wall_s": 157.0333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.7083, + "mean_secondary_score": 0.75, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4539845.0, + "mean_tokens_input": 2263375.0, + "mean_tokens_output": 13267.6667, + "mean_cost_usd": 0.7303, + "total_cost_usd": 2.191, + "contamination_suspect_runs": 0, + "mean_wall_s": 223.7, + "statuses": { + "turn_limit": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2242228.3333, + "mean_tokens_input": 1117914.0, + "mean_tokens_output": 6417.3333, + "mean_cost_usd": 1.306, + "total_cost_usd": 3.918, + "contamination_suspect_runs": 0, + "mean_wall_s": 153.4, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.6667, + "mean_primary_score": 0.9583, + "mean_secondary_score": 1.0, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 3467477.6667, + "mean_tokens_input": 1729058.3333, + "mean_tokens_output": 10344.3333, + "mean_cost_usd": 0.5968, + "total_cost_usd": 1.7904, + "contamination_suspect_runs": 0, + "mean_wall_s": 132.2667, + "statuses": { + "task_fail": 1, + "ok": 2 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 2368378.6667, + "mean_tokens_input": 1181231.6667, + "mean_tokens_output": 5931.0, + "mean_cost_usd": 1.4462, + "total_cost_usd": 4.3385, + "contamination_suspect_runs": 0, + "mean_wall_s": 122.2333, + "statuses": { + "ok": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.5417, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4584002.6667, + "mean_tokens_input": 2279746.3333, + "mean_tokens_output": 24678.6667, + "mean_cost_usd": 0.7666, + "total_cost_usd": 2.2999, + "contamination_suspect_runs": 0, + "mean_wall_s": 265.1333, + "statuses": { + "task_fail": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 2, + "n_valid": 2, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.875, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3298667.5, + "mean_tokens_input": 1642369.0, + "mean_tokens_output": 13953.0, + "mean_cost_usd": 1.7015, + "total_cost_usd": 3.403, + "contamination_suspect_runs": 0, + "mean_wall_s": 281.45, + "statuses": { + "task_fail": 2 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 0.0, + "mean_primary_score": 0.25, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.6667, + "skills_used": { + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 6091146.0, + "mean_tokens_input": 3032558.0, + "mean_tokens_output": 27575.3333, + "mean_cost_usd": 0.9306, + "total_cost_usd": 2.7918, + "contamination_suspect_runs": 0, + "mean_wall_s": 270.6667, + "statuses": { + "turn_limit": 3 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 3, + "n_excluded": 0, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 1.0, + "skills_used": { + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 4621712.3333, + "mean_tokens_input": 2303189.6667, + "mean_tokens_output": 15360.6667, + "mean_cost_usd": 2.6001, + "total_cost_usd": 7.8003, + "contamination_suspect_runs": 1, + "mean_wall_s": 618.1667, + "statuses": { + "ok": 3 + }, + "retries": 0 + } + ], + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 38, + "n_valid": 38, + "n_excluded": 0, + "pass_rate": 0.3421, + "mean_primary_score": 0.6555, + "mean_secondary_score": 0.6952, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3927619.0, + "mean_tokens_input": 1957367.2368, + "mean_tokens_output": 13036.8684, + "mean_cost_usd": 0.6481, + "total_cost_usd": 24.6268, + "contamination_suspect_runs": 18, + "mean_wall_s": 189.8421, + "statuses": { + "ok": 13, + "task_fail": 18, + "turn_limit": 7 + }, + "retries": 1 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 37, + "n_valid": 37, + "n_excluded": 0, + "pass_rate": 0.5405, + "mean_primary_score": 0.7959, + "mean_secondary_score": 0.7117, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2798291.8919, + "mean_tokens_input": 1395642.8378, + "mean_tokens_output": 7026.2432, + "mean_cost_usd": 1.4455, + "total_cost_usd": 53.482, + "contamination_suspect_runs": 7, + "mean_wall_s": 169.4514, + "statuses": { + "ok": 20, + "task_fail": 12, + "turn_limit": 5 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 36, + "n_valid": 36, + "n_excluded": 0, + "pass_rate": 0.8056, + "mean_primary_score": 0.9249, + "mean_secondary_score": 0.9167, + "trigger_rate": 0.9167, + "skills_used": { + "authoring-airflow-dags": 12, + "migrating-to-airflow-3": 14, + "testing-airflow-dags": 7 + }, + "mean_tokens_total": 3789705.3889, + "mean_tokens_input": 1889121.0833, + "mean_tokens_output": 11754.9722, + "mean_cost_usd": 0.6422, + "total_cost_usd": 23.1178, + "contamination_suspect_runs": 12, + "mean_wall_s": 158.9556, + "statuses": { + "ok": 29, + "task_fail": 6, + "turn_limit": 1 + }, + "retries": 0 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 33, + "n_valid": 33, + "n_excluded": 0, + "pass_rate": 0.9394, + "mean_primary_score": 0.9913, + "mean_secondary_score": 0.9293, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 15, + "migrating-to-airflow-3": 9, + "testing-airflow-dags": 9 + }, + "mean_tokens_total": 3476024.7879, + "mean_tokens_input": 1733759.8788, + "mean_tokens_output": 8526.4848, + "mean_cost_usd": 1.8306, + "total_cost_usd": 60.4112, + "contamination_suspect_runs": 16, + "mean_wall_s": 174.9727, + "statuses": { + "ok": 31, + "task_fail": 2 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 20, + "n_valid": 20, + "n_excluded": 0, + "pass_rate": 0.2, + "mean_primary_score": 0.5516, + "mean_secondary_score": 0.7167, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3804160.75, + "mean_tokens_input": 1894976.85, + "mean_tokens_output": 14353.95, + "mean_cost_usd": 0.676, + "total_cost_usd": 13.5193, + "contamination_suspect_runs": 10, + "mean_wall_s": 199.695, + "statuses": { + "ok": 4, + "turn_limit": 2, + "task_fail": 14 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 20, + "n_valid": 20, + "n_excluded": 0, + "pass_rate": 0.35, + "mean_primary_score": 0.7923, + "mean_secondary_score": 0.65, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2941579.7, + "mean_tokens_input": 1466632.35, + "mean_tokens_output": 8335.8, + "mean_cost_usd": 1.8258, + "total_cost_usd": 36.5163, + "contamination_suspect_runs": 1, + "mean_wall_s": 193.76, + "statuses": { + "ok": 7, + "task_fail": 12, + "cost_limit": 1 + }, + "retries": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 19, + "n_valid": 19, + "n_excluded": 0, + "pass_rate": 0.2632, + "mean_primary_score": 0.6787, + "mean_secondary_score": 0.8289, + "trigger_rate": 0.7895, + "skills_used": { + "authoring-airflow-dags": 7, + "migrating-to-airflow-3": 7, + "testing-airflow-dags": 2 + }, + "mean_tokens_total": 4469277.5789, + "mean_tokens_input": 2226742.8421, + "mean_tokens_output": 16166.0526, + "mean_cost_usd": 0.8257, + "total_cost_usd": 15.6879, + "contamination_suspect_runs": 6, + "mean_wall_s": 184.6263, + "statuses": { + "ok": 5, + "task_fail": 8, + "turn_limit": 6 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 18, + "n_valid": 18, + "n_excluded": 0, + "pass_rate": 0.7778, + "mean_primary_score": 0.9706, + "mean_secondary_score": 0.963, + "trigger_rate": 1.0, + "skills_used": { + "authoring-airflow-dags": 12, + "migrating-to-airflow-3": 3, + "testing-airflow-dags": 3 + }, + "mean_tokens_total": 3981957.6667, + "mean_tokens_input": 1986455.3889, + "mean_tokens_output": 9071.1667, + "mean_cost_usd": 2.4723, + "total_cost_usd": 44.5016, + "contamination_suspect_runs": 8, + "mean_wall_s": 245.2, + "statuses": { + "ok": 14, + "turn_limit": 1, + "task_fail": 3 + }, + "retries": 1 + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.4616, + "ci95_delta_pass_rate": [ + 0.2051, + 0.6923 + ], + "mean_delta_primary_score": 0.2664, + "ci95_delta_primary_score": [ + 0.0855, + 0.4491 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-clickstream-claim-check", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.3334, + "delta_primary_score": -0.0953 + }, + { + "case": "authoring-vendor-shipments-etl", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.0833 + }, + { + "case": "debugging-manual-trigger-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.1905 + }, + { + "case": "debugging-variable-at-parse", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "migration-inventory-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.4667 + }, + { + "case": "migration-revenue-previous-day", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.5238 + }, + { + "case": "migration-weekly-catchup", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.6667 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.3889 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.5714 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.2917 + }, + { + "case": "testing-custom-operator-conn", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.875 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.25 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.0476, + "ci95_delta_pass_rate": [ + 0.0, + 0.1428 + ], + "mean_delta_primary_score": 0.0868, + "ci95_delta_primary_score": [ + -0.1295, + 0.3647 + ], + "cases": [ + { + "case": "authoring-daily-revenue-yesterday", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.0556 + }, + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.2223 + }, + { + "case": "debugging-nondeterministic-parse", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "migration-hourly-pageviews-templates", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.1429 + }, + { + "case": "migration-shipments-assets", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.8571 + }, + { + "case": "scheduling-backfill-pageviews", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.0185 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": -0.2917 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 12, + "mean_delta_pass_rate": 0.3333, + "ci95_delta_pass_rate": [ + 0.1111, + 0.5833 + ], + "mean_delta_primary_score": 0.182, + "ci95_delta_primary_score": [ + 0.0571, + 0.3608 + ], + "cases": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-clickstream-claim-check", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "authoring-vendor-shipments-etl", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-manual-trigger-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.1429 + }, + { + "case": "debugging-variable-at-parse", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 1.0 + }, + { + "case": "migration-inventory-context", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2 + }, + { + "case": "migration-weekly-catchup", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2857 + }, + { + "case": "scheduling-idempotent-orders-load", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.2222 + }, + { + "case": "scheduling-weekday-ny-business-day", + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333, + "delta_primary_score": 0.3333 + }, + { + "case": "testing-airflow2-ci-suite", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "testing-custom-operator-conn", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "testing-dagbag-integrity", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + } + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 6, + "mean_delta_pass_rate": 0.3889, + "ci95_delta_pass_rate": [ + 0.0, + 0.7778 + ], + "mean_delta_primary_score": 0.137, + "ci95_delta_primary_score": [ + 0.0412, + 0.2275 + ], + "cases": [ + { + "case": "authoring-daily-revenue-yesterday", + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667, + "delta_primary_score": 0.2222 + }, + { + "case": "authoring-partner-feeds-mapping", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.0 + }, + { + "case": "debugging-nondeterministic-parse", + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333, + "delta_primary_score": -0.0333 + }, + { + "case": "migration-shipments-assets", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0, + "delta_primary_score": 0.2857 + }, + { + "case": "scheduling-backfill-pageviews", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.2222 + }, + { + "case": "testing-taskflow-transforms", + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0, + "delta_primary_score": 0.125 + } + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 13, + "n_pairs": 25, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.4, + "ci95_delta_pass_rate": [ + 0.2, + 0.6061 + ], + "mean_delta_primary_score": 0.2259, + "ci95_delta_primary_score": [ + 0.1013, + 0.3483 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 7, + "n_pairs": 13, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2051, + "ci95_delta_pass_rate": [ + 0.0, + 0.4286 + ], + "mean_delta_primary_score": 0.11, + "ci95_delta_primary_score": [ + -0.0266, + 0.2822 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ], + "failure_classes": { + "ok": 123, + "task_fail": 75, + "turn_limit": 22, + "cost_limit": 1 + }, + "total_cost_usd": 271.8629, + "isolation": { + "contamination_suspects": [ + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-yesterday-pyn34e58/ws/README.md", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/first_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/second_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/second_run.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_before.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_after.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_before.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/partner_feed_after.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SOLUTION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SOLUTION.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow-test-", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow-test-", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_verification.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_verification.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/detailed_changes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/detailed_changes.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/inv_test", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/dags/sql/daily_revenue.sql", + "root": "work-root" + }, + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-45309/migration-revenue-previous-day-hfc9t_wx/ws/data/orders.csv", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/MIGRATION_CHECKLIST.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/MIGRATION_CHECKLIST.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/CHANGES_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SCHEDULE_DIAGRAM.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/SCHEDULE_DIAGRAM.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CHANGES.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/analyze_shipments.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/analyze_shipments.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test.db", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb.duckdb", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/testdb2.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "write", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", + "root": "work-root" + }, + { + "tool": "bash", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/testing-custom-operator-conn-z5v0yj2h/ws/conftest.py", + "root": "work-root" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/landing", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-2026-09-30-baseline-20260930-baseline-20260930-045309/debugging-variable-at-parse-y19jdpne/ws/dags/warehouse_load.py", + "root": "work-root" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_after.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_after.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verification.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow-test-", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow-test-idempotent", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/baseline-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test2", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/dag_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/dag_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/authoring-daily-revenue-yesterday-ew4q6g1i/ws", + "root": "work-root" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/debugging-manual-trigger-context-1x63tzof/ws/dags/partner_feed.py", + "root": "work-root" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/upgrade_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/upgrade_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-182417/migration-inventory-context-im02tn8a/ws/dags/inventory_snapshot.py", + "root": "work-root" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "read", + "path": "/baseline-run3-2026-09-30-baseline-20260930-baseline-20260930-082417/migration-shipments-assets-bz1624jc/ws/dags/carrier_scorecard.py", + "root": "work-root" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/dag_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/dag_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/fx_pnl_changes_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/fx_pnl_changes_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_csp", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_dr2", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/deployment_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/deployment_guide.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "////airflow_vendor_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow_vendor_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "////airflow_vendor_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_vendor_home2_", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_warehouse", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "turn_limit", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_notes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_notes.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_baseline.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before_baseline.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after_verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after_verification.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/FINAL_MIGRATION_REPORT.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/FINAL_MIGRATION_REPORT.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_pv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-custom-operator-conn", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 1, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_airflow", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_1", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_rerun_test", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-27.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "turn_limit", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/verify_dag_structure.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.py", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": ".nymKSGkNb1", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-manual-trigger-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/warehouse_ingest_fix_summary.md", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/vendor_shipments_summary.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/requirements_checklist.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/requirements_checklist.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/usage_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/usage_guide.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/DELIVERY_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/DELIVERY_SUMMARY.md", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_pv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag_idempotent.sh", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_dag_idempotent.sh", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_test_inv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/snap_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/low_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/airflow_inv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/snap_before.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/low_before.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/first_run_2026-09-29.csv", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/first_run_2026-09-29.csv", + "root": "shared-temp" + } + ] + }, + { + "case": "testing-taskflow-transforms", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 2, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/orders_enrichment.py.bak", + "root": "shared-temp" + } + ] + }, + { + "case": "debugging-variable-at-parse", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-inventory-context", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-revenue-previous-day", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/migration_summary.txt", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-shipments-assets", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "migration-weekly-catchup", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_test2_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv_", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/af_pgv2", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "run": 3, + "status": "task_fail", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_pageviews_backfill.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_pageviews_backfill.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/BACKFILL_SETUP_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/BACKFILL_SETUP_SUMMARY.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/test_idempotency.py", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/VERIFICATION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/VERIFICATION.txt", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CONCERN_ANALYSIS.md", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": "/CONCERN_ANALYSIS.md", + "root": "shared-temp" + } + ] + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "run": 3, + "status": "ok", + "_source": "evals/airflow/results/skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + }, + { + "tool": "bash", + "path": ".IKuqtA2Qn0", + "root": "shared-temp" + } + ] + } + ], + "kill_command_runs": [], + "agent_env_restored_runs": [], + "agent_env_restore_failed_runs": [] + }, + "sources": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30" + }, + { + "dir": "evals/airflow/results/baseline-run3-2026-09-30" + }, + { + "dir": "evals/airflow/results/skill-2026-09-30" + } + ], + "n_runs": 221 +} \ No newline at end of file diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/meta.json b/evals/airflow/results/altimate-code-2026-09-30-summary/meta.json new file mode 100644 index 0000000..a273fd8 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/meta.json @@ -0,0 +1,163 @@ +{ + "generated_at": "2026-10-01T09:29:30+00:00", + "inputs": [ + "evals/airflow/results/baseline-2026-09-30", + "evals/airflow/results/baseline-run3-2026-09-30", + "evals/airflow/results/skill-2026-09-30" + ], + "index_dirs": [ + "evals/airflow/results/_smoke-haiku-baseline", + "evals/airflow/results/_smoke-haiku-skill", + "evals/airflow/results/_smoke-sonnet-baseline", + "evals/airflow/results/baseline-2026-09-30", + "evals/airflow/results/baseline-run3-2026-09-30", + "evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01", + "evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01", + "evals/airflow/results/cc-triggers-2026-10-01", + "evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01", + "evals/airflow/results/final-triggers-2026-10-01", + "evals/airflow/results/skill-2026-09-30", + "evals/airflow/results/triggers-2026-09-30" + ], + "work_root": "", + "harness_sha256": "20c2cab06bbe97cdd2577a662e6740733505cb930d0a6fcd1255437418d64d6c", + "current_airflow_skills_sha256": "a8dbbed8a1a806945b4148c2bd2be3220610ac7e5403737df42dc6743411ca0e", + "campaigns": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": null, + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T11:53:13+00:00" + }, + { + "dir": "evals/airflow/results/baseline-run3-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": null, + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T15:24:19+00:00" + }, + { + "dir": "evals/airflow/results/skill-2026-09-30", + "arm": "skill", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": "e03e9d6dbfd25d119b2586f509d4fce86bcfd8d5b243fa25994516fb246b0a19", + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T15:26:28+00:00" + } + ], + "n_attempts": 244, + "n_attempts_with_events": 244, + "n_runs": { + "all": 240, + "excluding-flagged": 143, + "excluding-strong": 221 + }, + "flag_counts": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 39, + "flagged": 19, + "strong": 1, + "no_events": 0, + "flagged_fail": 13, + "flagged_pass": 6 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 39, + "flagged": 9, + "strong": 2, + "no_events": 0, + "flagged_pass": 8, + "flagged_fail": 1 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 39, + "flagged": 15, + "strong": 3, + "no_events": 0, + "flagged_pass": 13, + "flagged_fail": 2 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 39, + "flagged": 22, + "strong": 6, + "no_events": 0, + "flagged_pass": 21, + "flagged_fail": 1 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 21, + "flagged": 11, + "strong": 1, + "no_events": 0, + "flagged_pass": 4, + "flagged_fail": 7 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 21, + "flagged": 2, + "strong": 1, + "no_events": 0, + "flagged_fail": 1, + "flagged_pass": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 21, + "flagged": 8, + "strong": 2, + "no_events": 0, + "flagged_fail": 7, + "flagged_pass": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 21, + "flagged": 11, + "strong": 3, + "no_events": 0, + "flagged_pass": 8, + "flagged_fail": 3 + } + ] +} \ No newline at end of file diff --git a/evals/airflow/results/altimate-code-2026-09-30-summary/results.json b/evals/airflow/results/altimate-code-2026-09-30-summary/results.json new file mode 100644 index 0000000..c498816 --- /dev/null +++ b/evals/airflow/results/altimate-code-2026-09-30-summary/results.json @@ -0,0 +1,3696 @@ +{ + "meta": { + "generated_at": "2026-10-01T09:29:30+00:00", + "inputs": [ + "evals/airflow/results/baseline-2026-09-30", + "evals/airflow/results/baseline-run3-2026-09-30", + "evals/airflow/results/skill-2026-09-30" + ], + "index_dirs": [ + "evals/airflow/results/_smoke-haiku-baseline", + "evals/airflow/results/_smoke-haiku-skill", + "evals/airflow/results/_smoke-sonnet-baseline", + "evals/airflow/results/baseline-2026-09-30", + "evals/airflow/results/baseline-run3-2026-09-30", + "evals/airflow/results/cc-baseline-sonnet-5-5-2026-10-01", + "evals/airflow/results/cc-skill-sonnet-5-5-2026-10-01", + "evals/airflow/results/cc-triggers-2026-10-01", + "evals/airflow/results/final-hard-baseline-claude-sonnet-5-5-2026-10-01", + "evals/airflow/results/final-triggers-2026-10-01", + "evals/airflow/results/skill-2026-09-30", + "evals/airflow/results/triggers-2026-09-30" + ], + "work_root": "", + "harness_sha256": "20c2cab06bbe97cdd2577a662e6740733505cb930d0a6fcd1255437418d64d6c", + "current_airflow_skills_sha256": "a8dbbed8a1a806945b4148c2bd2be3220610ac7e5403737df42dc6743411ca0e", + "campaigns": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": null, + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T11:53:13+00:00" + }, + { + "dir": "evals/airflow/results/baseline-run3-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": null, + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T15:24:19+00:00" + }, + { + "dir": "evals/airflow/results/skill-2026-09-30", + "arm": "skill", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runner": null, + "altimate_code_version": "0.12.2", + "airflow_skills_sha256": "e03e9d6dbfd25d119b2586f509d4fce86bcfd8d5b243fa25994516fb246b0a19", + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "started_at": "2026-09-30T15:26:28+00:00" + } + ], + "n_attempts": 244, + "n_attempts_with_events": 244, + "n_runs": { + "all": 240, + "excluding-flagged": 143, + "excluding-strong": 221 + }, + "flag_counts": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 39, + "flagged": 19, + "strong": 1, + "no_events": 0, + "flagged_fail": 13, + "flagged_pass": 6 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 39, + "flagged": 9, + "strong": 2, + "no_events": 0, + "flagged_pass": 8, + "flagged_fail": 1 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 39, + "flagged": 15, + "strong": 3, + "no_events": 0, + "flagged_pass": 13, + "flagged_fail": 2 + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 39, + "flagged": 22, + "strong": 6, + "no_events": 0, + "flagged_pass": 21, + "flagged_fail": 1 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 21, + "flagged": 11, + "strong": 1, + "no_events": 0, + "flagged_pass": 4, + "flagged_fail": 7 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 21, + "flagged": 2, + "strong": 1, + "no_events": 0, + "flagged_fail": 1, + "flagged_pass": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "attempts": 21, + "flagged": 8, + "strong": 2, + "no_events": 0, + "flagged_fail": 7, + "flagged_pass": 1 + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "attempts": 21, + "flagged": 11, + "strong": 3, + "no_events": 0, + "flagged_pass": 8, + "flagged_fail": 3 + } + ] + }, + "strong_attempts": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-vendor-shipments-etl", + "run": 3, + "status": "ok", + "source": "baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/landing", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "debugging-variable-at-parse", + "run": 3, + "status": "ok", + "source": "baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/baseline-run3-2026-09-30-baseline-20260930-082417", + "root": "work-root", + "first_access": "use", + "strength": "strong", + "n_prior_users": 0, + "prior_users": [], + "concurrent_writers": [] + } + ] + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "scheduling-backfill-pageviews", + "run": 3, + "status": "task_fail", + "source": "baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-custom-operator-conn", + "run": 3, + "status": "ok", + "source": "baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-taskflow-transforms", + "run": 3, + "status": "ok", + "source": "baseline-run3-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "debugging-variable-at-parse", + "run": 1, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 3, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + } + ] + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-hourly-pageviews-templates", + "run": 1, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 3, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 3, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-inventory-context", + "run": 1, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 4, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 4, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 5, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-revenue-previous-day", + "run": 1, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 6, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ] + } + ] + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-hourly-pageviews-templates", + "run": 2, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 11, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 11, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 11, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 11, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 13, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 13, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 13, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + } + ] + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-hourly-pageviews-templates", + "run": 2, + "status": "task_fail", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 12, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 13, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-revenue-previous-day", + "run": 2, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 16, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 17, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 17, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "scheduling-idempotent-orders-load", + "run": 2, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 3, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/airflow_test", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 3, + "prior_users": [ + "baseline-run3-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/scheduling-backfill-pageviews/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0", + "baseline-run3-2026-09-30/runs/testing-taskflow-transforms/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "debugging-variable-at-parse", + "run": 3, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test_warehouse", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 1, + "prior_users": [ + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-hourly-pageviews-templates", + "run": 3, + "status": "task_fail", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 18, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 18, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [] + } + ] + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-hourly-pageviews-templates", + "run": 3, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-revenue-previous-day", + "run": 3, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 20, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 24, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 20, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 21, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 25, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-shipments-assets/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-weekly-catchup", + "run": 3, + "status": "ok", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 26, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 26, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 26, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 26, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 22, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 23, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 23, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 23, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/before.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 27, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + }, + { + "tool": "bash", + "path": "/after.json", + "root": "shared-temp", + "first_access": "overwrite", + "strength": "strong", + "n_prior_users": 23, + "prior_users": [ + "skill-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/debugging-variable-at-parse/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-1", + "skill-2026-09-30/runs/migration-hourly-pageviews-templates/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-inventory-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0", + "skill-2026-09-30/runs/migration-revenue-previous-day/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0" + ], + "concurrent_writers": [ + "skill-2026-09-30/runs/migration-weekly-catchup/google-vertex-anthropic_claude-haiku-4-5_20251001/run-3/attempt-0" + ] + } + ] + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "testing-custom-operator-conn", + "run": 3, + "status": "turn_limit", + "source": "skill-2026-09-30", + "hits": [ + { + "tool": "bash", + "path": "/test.duckdb", + "root": "shared-temp", + "first_access": "use", + "strength": "strong", + "n_prior_users": 2, + "prior_users": [ + "baseline-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0", + "baseline-run3-2026-09-30/runs/testing-custom-operator-conn/google-vertex-anthropic_claude-sonnet-4-6_default/run-3/attempt-0" + ], + "concurrent_writers": [] + } + ] + } + ], + "tables": { + "all": { + "n_runs": 240, + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 39, + "pass_rate": 0.359, + "n_runs": 39, + "statuses": { + "ok": 14, + "task_fail": 18, + "turn_limit": 7 + } + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 39, + "pass_rate": 0.5641, + "n_runs": 39, + "statuses": { + "ok": 22, + "task_fail": 12, + "turn_limit": 5 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 39, + "pass_rate": 0.7949, + "n_runs": 39, + "statuses": { + "ok": 31, + "task_fail": 6, + "turn_limit": 2 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 39, + "pass_rate": 0.9487, + "n_runs": 39, + "statuses": { + "ok": 37, + "task_fail": 2 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 21, + "pass_rate": 0.1905, + "n_runs": 21, + "statuses": { + "ok": 4, + "turn_limit": 2, + "task_fail": 15 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 21, + "pass_rate": 0.381, + "n_runs": 21, + "statuses": { + "ok": 8, + "task_fail": 12, + "cost_limit": 1 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 21, + "pass_rate": 0.2381, + "n_runs": 21, + "statuses": { + "ok": 5, + "task_fail": 10, + "turn_limit": 6 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 21, + "pass_rate": 0.8095, + "n_runs": 21, + "statuses": { + "ok": 17, + "turn_limit": 1, + "task_fail": 3 + } + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.4359, + "ci95_delta_pass_rate": [ + 0.1795, + 0.6667 + ], + "mean_delta_primary_score": 0.2439, + "ci95_delta_primary_score": [ + 0.0773, + 0.4044 + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.0476, + "ci95_delta_pass_rate": [ + 0.0, + 0.1428 + ], + "mean_delta_primary_score": 0.1567, + "ci95_delta_primary_score": [ + -0.0675, + 0.415 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.3846, + "ci95_delta_pass_rate": [ + 0.1538, + 0.6154 + ], + "mean_delta_primary_score": 0.1863, + "ci95_delta_primary_score": [ + 0.0672, + 0.3517 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.4286, + "ci95_delta_pass_rate": [ + 0.0476, + 0.7619 + ], + "mean_delta_primary_score": 0.1727, + "ci95_delta_primary_score": [ + 0.0627, + 0.288 + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 13, + "n_pairs": 26, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.4103, + "ci95_delta_pass_rate": [ + 0.2051, + 0.6154 + ], + "mean_delta_primary_score": 0.2151, + "ci95_delta_primary_score": [ + 0.0974, + 0.3263 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 7, + "n_pairs": 14, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2381, + "ci95_delta_pass_rate": [ + 0.0238, + 0.4286 + ], + "mean_delta_primary_score": 0.1647, + "ci95_delta_primary_score": [ + 0.0231, + 0.3235 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ] + }, + "excluding-flagged": { + "n_runs": 143, + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 20, + "pass_rate": 0.4, + "n_runs": 20, + "statuses": { + "ok": 8, + "task_fail": 7, + "turn_limit": 5 + } + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 30, + "pass_rate": 0.4667, + "n_runs": 30, + "statuses": { + "ok": 14, + "task_fail": 11, + "turn_limit": 5 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 24, + "pass_rate": 0.75, + "n_runs": 24, + "statuses": { + "ok": 18, + "task_fail": 5, + "turn_limit": 1 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 17, + "pass_rate": 0.9412, + "n_runs": 17, + "statuses": { + "ok": 16, + "task_fail": 1 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 10, + "pass_rate": 0.0, + "n_runs": 10, + "statuses": { + "turn_limit": 2, + "task_fail": 8 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 19, + "pass_rate": 0.3684, + "n_runs": 19, + "statuses": { + "ok": 7, + "task_fail": 11, + "cost_limit": 1 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 13, + "pass_rate": 0.3077, + "n_runs": 13, + "statuses": { + "ok": 4, + "task_fail": 4, + "turn_limit": 5 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 10, + "pass_rate": 0.9, + "n_runs": 10, + "statuses": { + "ok": 9, + "task_fail": 1 + } + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 9, + "mean_delta_pass_rate": 0.4259, + "ci95_delta_pass_rate": [ + 0.0926, + 0.7778 + ], + "mean_delta_primary_score": 0.2667, + "ci95_delta_primary_score": [ + 0.0299, + 0.5093 + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 3, + "mean_delta_pass_rate": 0.1667, + "ci95_delta_pass_rate": [ + 0.0, + 0.5 + ], + "mean_delta_primary_score": 0.0139, + "ci95_delta_primary_score": [ + -0.2917, + 0.2778 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 6, + "mean_delta_pass_rate": 0.2222, + "ci95_delta_pass_rate": [ + 0.0555, + 0.3889 + ], + "mean_delta_primary_score": 0.1389, + "ci95_delta_primary_score": [ + 0.0278, + 0.25 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 4, + "mean_delta_pass_rate": 0.3333, + "ci95_delta_pass_rate": [ + -0.1666, + 0.8334 + ], + "mean_delta_primary_score": 0.0785, + "ci95_delta_primary_score": [ + -0.0167, + 0.1736 + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 10, + "n_pairs": 15, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.3444, + "ci95_delta_pass_rate": [ + 0.1146, + 0.6333 + ], + "mean_delta_primary_score": 0.2155, + "ci95_delta_primary_score": [ + 0.0536, + 0.4125 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 5, + "n_pairs": 7, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2619, + "ci95_delta_pass_rate": [ + -0.037, + 0.5556 + ], + "mean_delta_primary_score": 0.0508, + "ci95_delta_primary_score": [ + -0.0492, + 0.1519 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ] + }, + "excluding-strong": { + "n_runs": 221, + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 38, + "pass_rate": 0.3421, + "n_runs": 38, + "statuses": { + "ok": 13, + "task_fail": 18, + "turn_limit": 7 + } + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 37, + "pass_rate": 0.5405, + "n_runs": 37, + "statuses": { + "ok": 20, + "task_fail": 12, + "turn_limit": 5 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 36, + "pass_rate": 0.8056, + "n_runs": 36, + "statuses": { + "ok": 29, + "task_fail": 6, + "turn_limit": 1 + } + }, + { + "split": "dev", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 33, + "pass_rate": 0.9394, + "n_runs": 33, + "statuses": { + "ok": 31, + "task_fail": 2 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 20, + "pass_rate": 0.2, + "n_runs": 20, + "statuses": { + "ok": 4, + "turn_limit": 2, + "task_fail": 14 + } + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 20, + "pass_rate": 0.35, + "n_runs": 20, + "statuses": { + "ok": 7, + "task_fail": 12, + "cost_limit": 1 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_valid": 19, + "pass_rate": 0.2632, + "n_runs": 19, + "statuses": { + "ok": 5, + "task_fail": 8, + "turn_limit": 6 + } + }, + { + "split": "holdout", + "arm": "skill", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_valid": 18, + "pass_rate": 0.7778, + "n_runs": 18, + "statuses": { + "ok": 14, + "turn_limit": 1, + "task_fail": 3 + } + } + ], + "paired_deltas": [ + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "n_cases": 13, + "mean_delta_pass_rate": 0.4616, + "ci95_delta_pass_rate": [ + 0.2051, + 0.6923 + ], + "mean_delta_primary_score": 0.2664, + "ci95_delta_primary_score": [ + 0.0855, + 0.4491 + ] + }, + { + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "n_cases": 7, + "mean_delta_pass_rate": 0.0476, + "ci95_delta_pass_rate": [ + 0.0, + 0.1428 + ], + "mean_delta_primary_score": 0.0868, + "ci95_delta_primary_score": [ + -0.1295, + 0.3647 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "n_cases": 12, + "mean_delta_pass_rate": 0.3333, + "ci95_delta_pass_rate": [ + 0.1111, + 0.5833 + ], + "mean_delta_primary_score": 0.182, + "ci95_delta_primary_score": [ + 0.0571, + 0.3608 + ] + }, + { + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "n_cases": 6, + "mean_delta_pass_rate": 0.3889, + "ci95_delta_pass_rate": [ + 0.0, + 0.7778 + ], + "mean_delta_primary_score": 0.137, + "ci95_delta_primary_score": [ + 0.0412, + 0.2275 + ] + } + ], + "pooled_deltas": [ + { + "split": "dev", + "n_cases": 13, + "n_pairs": 25, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.4, + "ci95_delta_pass_rate": [ + 0.2, + 0.6061 + ], + "mean_delta_primary_score": 0.2259, + "ci95_delta_primary_score": [ + 0.1013, + 0.3483 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + }, + { + "split": "holdout", + "n_cases": 7, + "n_pairs": 13, + "models": [ + "google-vertex-anthropic/claude-haiku-4-5@20251001", + "google-vertex-anthropic/claude-sonnet-4-6@default" + ], + "mean_delta_pass_rate": 0.2051, + "ci95_delta_pass_rate": [ + 0.0, + 0.4286 + ], + "mean_delta_primary_score": 0.11, + "ci95_delta_primary_score": [ + -0.0266, + 0.2822 + ], + "ci_method": "percentile bootstrap over cases; a resampled case carries all its models" + } + ] + } + }, + "per_case_deltas": [ + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "authoring-airflow2-inventory-snapshot", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "authoring-clickstream-claim-check", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.3334 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.5, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.1667 + }, + "excluding-strong": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.3334 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "authoring-vendor-shipments-etl", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "debugging-manual-trigger-context", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "debugging-variable-at-parse", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-inventory-context", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-revenue-previous-day", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-weekly-catchup", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "scheduling-idempotent-orders-load", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "scheduling-weekday-ny-business-day", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "testing-airflow2-ci-suite", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "testing-custom-operator-conn", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.6667 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "testing-dagbag-integrity", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-airflow2-inventory-snapshot", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-clickstream-claim-check", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-vendor-shipments-etl", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "debugging-manual-trigger-context", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.5, + "delta_pass_rate": 0.5 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "debugging-variable-at-parse", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-inventory-context", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-revenue-previous-day", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-weekly-catchup", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "scheduling-idempotent-orders-load", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.5, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.5 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "scheduling-weekday-ny-business-day", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.3333 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-airflow2-ci-suite", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-custom-operator-conn", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-dagbag-integrity", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "authoring-daily-revenue-yesterday", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "authoring-partner-feeds-mapping", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "debugging-nondeterministic-parse", + "all": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.6667, + "skill_pass_rate": 0.6667, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-hourly-pageviews-templates", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "migration-shipments-assets", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "scheduling-backfill-pageviews", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.5, + "delta_pass_rate": 0.5 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.3333, + "delta_pass_rate": 0.3333 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "case": "testing-taskflow-transforms", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-daily-revenue-yesterday", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + }, + "excluding-strong": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "authoring-partner-feeds-mapping", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "debugging-nondeterministic-parse", + "all": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + }, + "excluding-flagged": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + }, + "excluding-strong": { + "baseline_pass_rate": 1.0, + "skill_pass_rate": 0.6667, + "delta_pass_rate": -0.3333 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-hourly-pageviews-templates", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "migration-shipments-assets", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 0.0, + "delta_pass_rate": 0.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "scheduling-backfill-pageviews", + "all": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "case": "testing-taskflow-transforms", + "all": { + "baseline_pass_rate": 0.3333, + "skill_pass_rate": 1.0, + "delta_pass_rate": 0.6667 + }, + "excluding-flagged": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + }, + "excluding-strong": { + "baseline_pass_rate": 0.0, + "skill_pass_rate": 1.0, + "delta_pass_rate": 1.0 + } + } + ], + "conclusion_check": [ + { + "split": "dev", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "all": { + "delta": 0.4359, + "ci95": [ + 0.1795, + 0.6667 + ], + "n_cases": 13 + }, + "excluding-flagged": { + "delta": 0.4259, + "ci95": [ + 0.0926, + 0.7778 + ], + "n_cases": 9 + }, + "sign_same": true, + "significance_same": true, + "changed": false + }, + { + "split": "dev", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "all": { + "delta": 0.3846, + "ci95": [ + 0.1538, + 0.6154 + ], + "n_cases": 13 + }, + "excluding-flagged": { + "delta": 0.2222, + "ci95": [ + 0.0555, + 0.3889 + ], + "n_cases": 6 + }, + "sign_same": true, + "significance_same": true, + "changed": false + }, + { + "split": "dev", + "model": "pooled", + "all": { + "delta": 0.4103, + "ci95": [ + 0.2051, + 0.6154 + ], + "n_cases": 13 + }, + "excluding-flagged": { + "delta": 0.3444, + "ci95": [ + 0.1146, + 0.6333 + ], + "n_cases": 10 + }, + "sign_same": true, + "significance_same": true, + "changed": false + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "all": { + "delta": 0.0476, + "ci95": [ + 0.0, + 0.1428 + ], + "n_cases": 7 + }, + "excluding-flagged": { + "delta": 0.1667, + "ci95": [ + 0.0, + 0.5 + ], + "n_cases": 3 + }, + "sign_same": true, + "significance_same": true, + "changed": false + }, + { + "split": "holdout", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "all": { + "delta": 0.4286, + "ci95": [ + 0.0476, + 0.7619 + ], + "n_cases": 7 + }, + "excluding-flagged": { + "delta": 0.3333, + "ci95": [ + -0.1666, + 0.8334 + ], + "n_cases": 4 + }, + "sign_same": true, + "significance_same": false, + "changed": true + }, + { + "split": "holdout", + "model": "pooled", + "all": { + "delta": 0.2381, + "ci95": [ + 0.0238, + 0.4286 + ], + "n_cases": 7 + }, + "excluding-flagged": { + "delta": 0.2619, + "ci95": [ + -0.037, + 0.5556 + ], + "n_cases": 5 + }, + "sign_same": true, + "significance_same": false, + "changed": true + } + ] +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/inventory-baseline.json b/evals/airflow/results/baseline-2026-09-30/inventory-baseline.json new file mode 100644 index 0000000..3cfca55 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/inventory-baseline.json @@ -0,0 +1,405 @@ +[ + { + "name": "altimate-code", + "description": "Delegates dbt and warehouse work to altimate-code, a specialized CLI agent with 100+ purpose-built data tools. USE THIS SKILL FIRST whenever the task mentions or implies: warehouse access (Snowflake, BigQuery, Redshift, Databricks, Postgres, MySQL, DuckDB), column-level lineage, downstream-impact analysis, dbt builds against a real warehouse, cross-database migration or validation, query cost attribution, schema diff, data parity checking, PII detection from sampled rows, incremental/SCD2/backfill verification, FinOps reporting, model documentation generation, query optimization, anomaly detection, dev-vs-prod diffing, or when the user says \"altimate\", \"altimate-code\", or \"the data agent\". Strongly preferred over Read/Write/Edit whenever the task touches live data, lineage, or warehouse state \u2014 consult before doing the work yourself.", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/altimate-code/SKILL.md", + "tools": [ + { + "name": "altimate-code", + "available": true + } + ] + }, + { + "name": "altimate-setup", + "description": "Configure Altimate platform credentials for datamate and API access", + "source": "builtin", + "location": "builtin:altimate-setup/SKILL.md", + "tools": [] + }, + { + "name": "cost-report", + "description": "Analyze Snowflake query costs and identify optimization opportunities", + "source": "builtin", + "location": "builtin:cost-report/SKILL.md", + "tools": [] + }, + { + "name": "creating-dbt-models", + "description": "Creates dbt models following project conventions. Use when working with dbt models for:\n(1) Creating new models (any layer - discovers project's naming conventions first)\n(2) Task mentions \"create\", \"build\", \"add\", \"write\", \"new\", or \"implement\" with model, table, or SQL\n(3) Modifying existing model logic, columns, joins, or transformations\n(4) Implementing a model from schema.yml specs or expected output requirements\nDiscovers project conventions before writing. Runs dbt build (not just compile) to verify.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/creating-dbt-models/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + }, + { + "name": "select", + "available": false + }, + { + "name": "from", + "available": false + }, + { + "name": "where", + "available": false + } + ] + }, + { + "name": "customize-opencode", + "description": "Use ONLY when the user is editing or creating Altimate Code's own configuration: altimate-code.json, opencode.json, opencode.jsonc, files under .altimate-code/, files under .opencode/, or files under ~/.config/altimate-code/. Also use when creating or fixing Altimate Code agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring Altimate Code itself.", + "source": "project", + "location": "", + "tools": [] + }, + { + "name": "data-parity", + "description": "Validate that two tables or query results are identical \u2014 or diagnose exactly how they differ. Discover schema, identify keys, profile cheaply, then diff. Use for migration validation, ETL regression, and query refactor verification.", + "source": "builtin", + "location": "builtin:data-parity/SKILL.md", + "tools": [] + }, + { + "name": "data-viz", + "description": "Build modern, interactive data visualizations and dashboards using code-based component libraries (shadcn/ui, Recharts, Tremor, Nivo, D3, Victory, visx). Use this skill whenever the user asks to visualize data, build dashboards, create analytics views, chart metrics, tell a data story, build a reporting interface, create KPI cards, plot graphs, or explore a dataset \u2014 even if they mention PowerBI, Tableau, Streamlit, Metabase, Looker, Grafana, or similar tools. Also trigger when the user says \"make a dashboard\", \"show me the data\", \"chart this\", \"visualize trends\", \"build an analytics page\", \"data story\", or anything involving turning raw data into interactive visual interfaces. If the task involves presenting data visually \u2014 this is the skill. Always prefer building a real, interactive, code-based UI over exporting to or recommending a BI platform.\n", + "source": "builtin", + "location": "builtin:data-viz/SKILL.md", + "tools": [] + }, + { + "name": "dbt-analyze", + "description": "Analyze downstream impact of dbt model changes using column-level lineage and the dependency graph. Use when evaluating the blast radius of a change before shipping. Powered by altimate-dbt.", + "source": "builtin", + "location": "builtin:dbt-analyze/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + } + ] + }, + { + "name": "dbt-develop", + "description": "REQUIRED before writing or modifying ANY dbt model. Invoke this skill FIRST\nwhenever a task says \"create\", \"build\", \"add\", \"modify\", \"update\", \"fix\", or\n\"refactor\" a dbt model, staging file, mart, incremental, or snapshot.\n\nSkipping this skill is the leading cause of silent-correctness bugs \u2014\nmodels that compile and `dbt build` cleanly but produce wrong values. It\ncontains the patterns that prevent the most common such bugs encountered\nin real dbt projects:\n\n\u2022 Incremental high-water marks (`>=` vs `>` ties \u2192 silent row dropout)\n\u2022 Snapshot strategy selection (timestamp vs check, `unique_key` choice)\n\u2022 `LEFT JOIN + COUNT(*)` phantom rows from unmatched parents\n\u2022 Type harmonization in `COALESCE` / `CASE` / `UNION` legs\n\u2022 Date-spine completeness (every period present, even empty ones)\n\u2022 Off-by-one window boundaries (`BETWEEN d - (N-1) AND d` for N-wide)\n\u2022 Uniqueness enforcement when schema implies a key\n\u2022 Window-function `LIMIT` with deterministic tiebreaker\n\u2022 Verifying transformation correctness with dbt unit tests, not just `dbt build`\n\u2022 Enumerating every requested deliverable and checking each exists on disk\n\nDo not start writing SQL until this skill is loaded. Powered by altimate-dbt.\n", + "source": "builtin", + "location": "builtin:dbt-develop/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + } + ] + }, + { + "name": "dbt-docs", + "description": "Document dbt models and columns in schema.yml with business context \u2014 model descriptions, column definitions, and doc blocks. Use when adding or improving documentation for discoverability. Powered by altimate-dbt.", + "source": "builtin", + "location": "builtin:dbt-docs/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + } + ] + }, + { + "name": "dbt-pr-review", + "description": "Cloudflare-style AI code review for dbt/SQL pull requests. Produces a signed APPROVE/COMMENT/REQUEST_CHANGES verdict where every blocking finding is backed by a deterministic engine call \u2014 column-lineage blast radius, query equivalence, PII classification, and A\u2013F grade. Use to review a dbt PR or the working-tree changes before merge.", + "source": "builtin", + "location": "builtin:dbt-pr-review/SKILL.md", + "tools": [ + { + "name": "dbt_pr_review", + "available": false + }, + { + "name": "impact_analysis", + "available": false + }, + { + "name": "altimate_core_equivalence", + "available": false + }, + { + "name": "altimate_core_check", + "available": false + }, + { + "name": "lineage_check", + "available": false + } + ] + }, + { + "name": "dbt-schema-verify", + "description": "REQUIRED after building or modifying ANY dbt model that has columns declared\nin `schema.yml` / `_models.yml`. Run `altimate-dbt schema-verify --model\n` to diff actual columns against the spec, and treat any `mismatch`\nverdict as \"not done.\"\n\nThe most common reason \"the build is green but the tests still fail\" is\nthat the model produces the right *data values* in the wrong *column\nshape* \u2014 extra columns, missing columns, wrong order, wrong types. Many\ndbt equality tests grade the column tuple `(name, type, position)`\nexactly, and the agent's prior bias is to add \"helpful\" extras\n(`p1`/`p2`/`p3` rank breakdowns, name-resolved variants, lineage\nmetadata) or reorder columns \"more logically.\" Both break the contract.\n\nThis skill enforces the mechanical check that catches those bugs before\ndeclaring done. Use it before declaring any model task complete.\n", + "source": "builtin", + "location": "builtin:dbt-schema-verify/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + }, + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "dbt-test", + "description": "Add schema tests, unit tests, and data quality checks to dbt models. Use when validating data integrity, adding test definitions to schema.yml, writing unit tests, or practicing test-driven development in dbt. Powered by altimate-dbt.", + "source": "builtin", + "location": "builtin:dbt-test/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + } + ] + }, + { + "name": "dbt-troubleshoot", + "description": "Debug dbt errors \u2014 compilation failures, runtime database errors, test failures, wrong data, and performance issues. Use when something is broken, producing wrong results, or failing to build. Powered by altimate-dbt.", + "source": "builtin", + "location": "builtin:dbt-troubleshoot/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + }, + { + "name": "altimate_core_semantics", + "available": false + }, + { + "name": "altimate_core_column_lineage", + "available": false + }, + { + "name": "altimate_core_correct", + "available": false + } + ] + }, + { + "name": "dbt-unit-tests", + "description": "Generate dbt unit tests automatically for any model. Analyzes SQL logic (CASE/WHEN, JOINs, window functions, NULLs), creates type-correct mock inputs from manifest schema, and assembles complete YAML. Use when a user says \"generate tests\", \"add unit tests\", \"test this model\", or \"test coverage\" for dbt models.", + "source": "builtin", + "location": "builtin:dbt-unit-tests/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + }, + { + "name": "dbt_unit_test_gen", + "available": false + } + ] + }, + { + "name": "debugging-dbt-errors", + "description": "Debugs and fixes dbt errors systematically. Use when working with dbt errors for:\n(1) Task mentions \"fix\", \"error\", \"broken\", \"failing\", \"debug\", \"wrong\", or \"not working\"\n(2) Compilation Error, Database Error, or test failures occur\n(3) Model produces incorrect output or unexpected results\n(4) Need to troubleshoot why a dbt command failed\nReads full error, checks upstream first, runs dbt build (not just compile) to verify fix.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/debugging-dbt-errors/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "developing-incremental-models", + "description": "Develops and troubleshoots dbt incremental models. Use when working with incremental materialization for:\n(1) Creating new incremental models (choosing strategy, unique_key, partition)\n(2) Task mentions \"incremental\", \"append\", \"merge\", \"upsert\", or \"late arriving data\"\n(3) Troubleshooting incremental failures (merge errors, partition pruning, schema drift)\n(4) Optimizing incremental performance or deciding table vs incremental\nGuides through strategy selection, handles common incremental gotchas.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/developing-incremental-models/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + }, + { + "name": "select", + "available": false + }, + { + "name": "min", + "available": false + }, + { + "name": "max", + "available": false + }, + { + "name": "count", + "available": false + }, + { + "name": "from", + "available": false + }, + { + "name": "group", + "available": false + }, + { + "name": "having", + "available": false + }, + { + "name": "limit", + "available": false + } + ] + }, + { + "name": "documenting-dbt-models", + "description": "Documents dbt models and columns in schema.yml. Use when working with dbt documentation for:\n(1) Adding model descriptions or column definitions to schema.yml\n(2) Task mentions \"document\", \"describe\", \"description\", \"dbt docs\", or \"schema.yml\"\n(3) Explaining business context, grain, meaning of data, or business rules\n(4) Preparing dbt docs generate or improving model discoverability\nMatches existing project documentation style and conventions before writing.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/documenting-dbt-models/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "finding-expensive-queries", + "description": "Finds and ranks expensive Snowflake queries by cost, time, or data scanned. Use when:\n(1) User asks to find slow, expensive, or problematic queries\n(2) Task mentions \"query history\", \"top queries\", \"most expensive\", or \"slowest queries\"\n(3) Analyzing warehouse costs or identifying optimization candidates\n(4) Finding queries that scan the most data or have the most spillage\nReturns ranked list of queries with metrics and optimization recommendations.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/snowflake/finding-expensive-queries/SKILL.md", + "tools": [] + }, + { + "name": "lineage-diff", + "description": "Compare column-level lineage between two versions of a SQL query to show added, removed, and changed data flow edges.", + "source": "builtin", + "location": "builtin:lineage-diff/SKILL.md", + "tools": [] + }, + { + "name": "migrating-sql-to-dbt", + "description": "Converts legacy SQL to modular dbt models. Use when migrating SQL to dbt for:\n(1) Converting stored procedures, views, or raw SQL files to dbt models\n(2) Task mentions \"migrate\", \"convert\", \"legacy SQL\", \"transform to dbt\", or \"modernize\"\n(3) Breaking monolithic queries into modular layers (discovers project conventions first)\n(4) Porting existing data pipelines or ETL to dbt patterns\nChecks for existing models/sources, builds and validates layer by layer.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/migrating-sql-to-dbt/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "optimizing-databricks-sql", + "description": "Analyze DBSQL queries, including SQL embedded in notebooks (`spark.sql(...)`, `%sql` cells), for anti-patterns, lint issues, and performance problems, using Databricks-specific dialect and platform knowledge (Delta, Photon, Unity Catalog) layered on top of altimate-code's generic SQL engine. Use when a user asks to optimize, review, or lint DBSQL queries on Databricks, whether standalone or embedded in a notebook.", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/databricks/optimizing-databricks-sql/SKILL.md", + "tools": [] + }, + { + "name": "optimizing-query-by-id", + "description": "Optimizes Snowflake query performance using query ID from history. Use when optimizing Snowflake queries for:\n(1) User provides a Snowflake query_id (UUID format) to analyze or optimize\n(2) Task mentions \"slow query\", \"optimize\", \"query history\", or \"query profile\" with a query ID\n(3) Analyzing query performance metrics - bytes scanned, spillage, partition pruning\n(4) User references a previously run query that needs optimization\nFetches query profile, identifies bottlenecks, returns optimized SQL with expected improvements.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/snowflake/optimizing-query-by-id/SKILL.md", + "tools": [] + }, + { + "name": "optimizing-query-text", + "description": "Optimizes Snowflake SQL query performance from provided query text. Use when optimizing Snowflake SQL for:\n(1) User provides or pastes a SQL query and asks to optimize, tune, or improve it\n(2) Task mentions \"slow query\", \"make faster\", \"improve performance\", \"optimize SQL\", or \"query tuning\"\n(3) Reviewing SQL for performance anti-patterns (function on filter column, implicit joins, etc.)\n(4) User asks why a query is slow or how to speed it up\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/snowflake/optimizing-query-text/SKILL.md", + "tools": [] + }, + { + "name": "pii-audit", + "description": "Classify schema columns for PII (SSN, email, phone, name, address, credit card) and check whether queries expose them. Use for GDPR/CCPA/HIPAA compliance audits.", + "source": "builtin", + "location": "builtin:pii-audit/SKILL.md", + "tools": [] + }, + { + "name": "query-optimize", + "description": "Analyze and optimize SQL queries for better performance", + "source": "builtin", + "location": "builtin:query-optimize/SKILL.md", + "tools": [] + }, + { + "name": "refactoring-dbt-models", + "description": "Safely refactors dbt models with downstream impact analysis. Use when restructuring dbt models for:\n(1) Task mentions \"refactor\", \"restructure\", \"extract\", \"split\", \"break into\", or \"reorganize\"\n(2) Extracting CTEs to intermediate models or creating macros\n(3) Modifying model logic that has downstream consumers\n(4) Renaming columns, changing types, or reorganizing model dependencies\nAnalyzes all downstream dependencies BEFORE making changes.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/refactoring-dbt-models/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "schema-migration", + "description": "Analyze DDL migrations for data loss risks \u2014 type narrowing, missing defaults, dropped constraints, breaking column changes. Use before applying schema changes to production.", + "source": "builtin", + "location": "builtin:schema-migration/SKILL.md", + "tools": [] + }, + { + "name": "sql-review", + "description": "Pre-merge SQL quality gate \u2014 lint 26 anti-patterns, grade readability/performance A-F, validate syntax, and scan for injection threats. Use before committing or reviewing SQL changes.", + "source": "builtin", + "location": "builtin:sql-review/SKILL.md", + "tools": [ + { + "name": "altimate-dbt", + "available": true + } + ] + }, + { + "name": "sql-translate", + "description": "Translate SQL queries between database dialects (Snowflake, BigQuery, PostgreSQL, MySQL, etc.)", + "source": "builtin", + "location": "builtin:sql-translate/SKILL.md", + "tools": [] + }, + { + "name": "teach", + "description": "Teach your AI teammate a pattern by showing it an example file from your codebase", + "source": "builtin", + "location": "builtin:teach/SKILL.md", + "tools": [] + }, + { + "name": "testing-dbt-models", + "description": "Adds schema tests and data quality validation to dbt models. Use when working with dbt tests for:\n(1) Adding or modifying tests in schema.yml files\n(2) Task mentions \"test\", \"validate\", \"data quality\", \"unique\", \"not_null\", or \"accepted_values\"\n(3) Ensuring data integrity - primary keys, foreign keys, relationships\n(4) Debugging test failures or understanding why dbt test failed\nMatches existing project test patterns and YAML style before adding new tests.\n", + "source": "project", + "location": "/baseline-2026-09-30-baseline-20260930-045309/skills/dbt/testing-dbt-models/SKILL.md", + "tools": [ + { + "name": "dbt", + "available": true + } + ] + }, + { + "name": "train", + "description": "Train your AI teammate on team standards from a document or style guide", + "source": "builtin", + "location": "builtin:train/SKILL.md", + "tools": [] + }, + { + "name": "training-status", + "description": "Show what your AI teammate has learned \u2014 patterns, rules, glossary, and standards", + "source": "builtin", + "location": "builtin:training-status/SKILL.md", + "tools": [] + } +] \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/meta.json b/evals/airflow/results/baseline-2026-09-30/meta.json new file mode 100644 index 0000000..c70b7b0 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/meta.json @@ -0,0 +1,348 @@ +{ + "run_name": "baseline-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "runs_per_case": 3, + "split": "all", + "argv": [ + "evals/harness/run_eval.py", + "--cases", + "evals/airflow/cases", + "--arm", + "baseline", + "--models", + "google-vertex-anthropic/claude-sonnet-4-6@default,google-vertex-anthropic/claude-haiku-4-5@20251001", + "--runs", + "3", + "--parallel", + "4", + "--split", + "all", + "--max-cost-usd", + "80", + "--out", + "evals/airflow/results/baseline-2026-09-30" + ], + "started_at": "2026-09-30T11:53:13+00:00", + "altimate_code_version": "0.12.2", + "small_model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "envs": { + "2.11": { + "python": "3.12.8", + "airflow": "2.11.2", + "pytest": "9.1.1", + "duckdb": "1.4.4", + "pandas": "2.1.4", + "ruff": "0.16.9", + "python_path": "~/.cache/des-evals/airflow-2.11/bin/python" + }, + "3.3": { + "python": "3.12.8", + "airflow": "3.3.2", + "pytest": "9.1.1", + "duckdb": "1.5.6", + "pandas": "3.0.5", + "ruff": "0.16.9", + "python_path": "~/.cache/des-evals/airflow-3.3/bin/python" + } + }, + "cases": { + "authoring-airflow2-inventory-snapshot": { + "sha256": "ae584c8885a3231aefbda92cbd0e3a71defa32bb3b25c2ee3186fa850283ca0d", + "area": "authoring", + "split": "dev", + "airflow_version": "2.11" + }, + "authoring-clickstream-claim-check": { + "sha256": "37a8ae627611237474b83cf5466d569d495cd0f9a685a1378c88484a6ab7e2fe", + "area": "authoring", + "split": "dev", + "airflow_version": "3.3" + }, + "authoring-daily-revenue-yesterday": { + "sha256": "4642d8bf21aab04711921f58ecb6a41b1b4ecf154034c7c5530766fa8f3f75d3", + "area": "authoring", + "split": "holdout", + "airflow_version": "3.3" + }, + "authoring-partner-feeds-mapping": { + "sha256": "d980d3caa6e3e9349e444bf1839b5e2b125e0b211599d1d9280b8e136daf8c46", + "area": "authoring", + "split": "holdout", + "airflow_version": "3.3" + }, + "authoring-vendor-shipments-etl": { + "sha256": "a4b4ec3598fde2bdd0861c6ac07d869a4fb9c9cd4f7e11afc1c80d148807cb3f", + "area": "authoring", + "split": "dev", + "airflow_version": "3.3" + }, + "debugging-manual-trigger-context": { + "sha256": "8338c7cee702b4f7e5fb5aeebb27e86a7e5e3480033c6262d992ca0dfb2e0012", + "area": "debugging", + "split": "dev", + "airflow_version": "3.3" + }, + "debugging-nondeterministic-parse": { + "sha256": "1bf8b7694a8ad4a6615d6d8f254d981dc83d01a972ce7498913ac4ee6de692f0", + "area": "debugging", + "split": "holdout", + "airflow_version": "3.3" + }, + "debugging-variable-at-parse": { + "sha256": "436770cb552cd4b75a7f97e40679e325aefe4cfd9ffe3b056e3e7edd3904b7a3", + "area": "debugging", + "split": "dev", + "airflow_version": "3.3" + }, + "migration-hourly-pageviews-templates": { + "sha256": "eac1e05c242883e2ba514c9a40eada3ca3a3eea85964e1dc941f730c22f471a9", + "area": "migration", + "split": "holdout", + "airflow_version": "3.3" + }, + "migration-inventory-context": { + "sha256": "7ceaba6f5bf122cc4ad72f52907a5ca590f2f8fd453510481ee4cd0bfeb2684e", + "area": "migration", + "split": "dev", + "airflow_version": "3.3" + }, + "migration-revenue-previous-day": { + "sha256": "9515ef4db5d2e875160dd43bc05c7486a225d4c0e9d5bf327062eda80731b4a4", + "area": "migration", + "split": "dev", + "airflow_version": "3.3" + }, + "migration-shipments-assets": { + "sha256": "eb3b442aa66d7ef010f409e021cec3aab4f2ea405b3595e6a59702c2fb08f817", + "area": "migration", + "split": "holdout", + "airflow_version": "3.3" + }, + "migration-weekly-catchup": { + "sha256": "33eb41343603e6636fbedb532f638c14d7344557b72de6453c7b4743b360ef65", + "area": "migration", + "split": "dev", + "airflow_version": "3.3" + }, + "scheduling-backfill-pageviews": { + "sha256": "21f1a5b92665ac392eee3645c12f5bec695a1bca968e69048e95da056f6a32fd", + "area": "scheduling", + "split": "holdout", + "airflow_version": "3.3" + }, + "scheduling-idempotent-orders-load": { + "sha256": "d7be51f944fc210339eb4ba7af7cddc57a443a4ea1b5bbebaffe7050d55b8b50", + "area": "scheduling", + "split": "dev", + "airflow_version": "2.11" + }, + "scheduling-weekday-ny-business-day": { + "sha256": "1b164415c1d78cbbcc41b74b4e8ad603477667f8021035b233d98d374acbbbe6", + "area": "scheduling", + "split": "dev", + "airflow_version": "3.3" + }, + "testing-airflow2-ci-suite": { + "sha256": "e9a78ab26628508f469970c94d20f18ce2f867ea9c781ec705ea858a21ae19f8", + "area": "testing", + "split": "dev", + "airflow_version": "2.11" + }, + "testing-custom-operator-conn": { + "sha256": "16b1915f0ea41351962b8c9bed5cd993ab60e878915fca081a9d1a8f58275e81", + "area": "testing", + "split": "dev", + "airflow_version": "3.3" + }, + "testing-dagbag-integrity": { + "sha256": "b33916eeb24d1bad516adc8b2371cdc46c4ba8951622c03999c93862aabebe27", + "area": "testing", + "split": "dev", + "airflow_version": "3.3" + }, + "testing-taskflow-transforms": { + "sha256": "3e703799455d8ba3eaad83795358beeb387898fd0504ab751e27f5fc0b717acc", + "area": "testing", + "split": "holdout", + "airflow_version": "3.3" + } + }, + "staged_skills_sha256": "ca2b482d4ec517502521a5686cd32137c1da615ed31db7a7fa543f031b4050f0", + "airflow_skills_dir": "/skills/airflow", + "airflow_skills_sha256": null, + "airflow_skill_names": [], + "repo_skill_names": [ + "optimizing-databricks-sql", + "creating-dbt-models", + "debugging-dbt-errors", + "developing-incremental-models", + "documenting-dbt-models", + "migrating-sql-to-dbt", + "refactoring-dbt-models", + "testing-dbt-models", + "finding-expensive-queries", + "optimizing-query-by-id", + "optimizing-query-text" + ], + "inventory": [ + [ + "altimate-code", + "project" + ], + [ + "altimate-setup", + "builtin" + ], + [ + "cost-report", + "builtin" + ], + [ + "creating-dbt-models", + "project" + ], + [ + "customize-opencode", + "project" + ], + [ + "data-parity", + "builtin" + ], + [ + "data-viz", + "builtin" + ], + [ + "dbt-analyze", + "builtin" + ], + [ + "dbt-develop", + "builtin" + ], + [ + "dbt-docs", + "builtin" + ], + [ + "dbt-pr-review", + "builtin" + ], + [ + "dbt-schema-verify", + "builtin" + ], + [ + "dbt-test", + "builtin" + ], + [ + "dbt-troubleshoot", + "builtin" + ], + [ + "dbt-unit-tests", + "builtin" + ], + [ + "debugging-dbt-errors", + "project" + ], + [ + "developing-incremental-models", + "project" + ], + [ + "documenting-dbt-models", + "project" + ], + [ + "finding-expensive-queries", + "project" + ], + [ + "lineage-diff", + "builtin" + ], + [ + "migrating-sql-to-dbt", + "project" + ], + [ + "optimizing-databricks-sql", + "project" + ], + [ + "optimizing-query-by-id", + "project" + ], + [ + "optimizing-query-text", + "project" + ], + [ + "pii-audit", + "builtin" + ], + [ + "query-optimize", + "builtin" + ], + [ + "refactoring-dbt-models", + "project" + ], + [ + "schema-migration", + "builtin" + ], + [ + "sql-review", + "builtin" + ], + [ + "sql-translate", + "builtin" + ], + [ + "teach", + "builtin" + ], + [ + "testing-dbt-models", + "project" + ], + [ + "train", + "builtin" + ], + [ + "training-status", + "builtin" + ] + ], + "harness_sha256": "8a34b5a45a17fc7b278e59e1fcc16d6a65419e7865d4eda84d04734f28478eae", + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "host": { + "platform": "macOS-26.5.2-arm64-arm-64bit", + "python": "3.12.8" + }, + "budget": { + "cap_usd": 80.0, + "per_run_cap_usd": 5.0, + "spent_usd": 83.57, + "stopped_early": true + }, + "finished_at": "2026-09-30T13:20:21+00:00", + "status_counts": { + "ok": 32, + "skipped_budget": 40, + "task_fail": 38, + "turn_limit": 10 + } +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/report/REPORT.md b/evals/airflow/results/baseline-2026-09-30/report/REPORT.md new file mode 100644 index 0000000..99d9519 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/report/REPORT.md @@ -0,0 +1,75 @@ +# Airflow skill eval report + +Pass rate = fraction of valid runs whose primary checks all passed. Excluded runs (infra_error, grader_error, skipped_budget) are listed but not counted. + +## dev: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 39 (26) | 38% | 0.664 | 0.676 | 0% | 3.99e+06 | 0.616 | 211 | {'ok': 10, 'task_fail': 11, 'turn_limit': 5, 'skipped_budget': 13} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 39 (26) | 58% | 0.822 | 0.705 | 0% | 2.83e+06 | 1.43 | 188 | {'ok': 15, 'task_fail': 8, 'turn_limit': 3, 'skipped_budget': 13} | + +## dev: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 100% | 1 | 0.667 | - | 0.373 | {'ok': 2, 'skipped_budget': 1} | +| authoring-airflow2-inventory-snapshot | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.667 | - | 0.734 | {'ok': 2, 'skipped_budget': 1} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.929 | 0.833 | - | 0.571 | {'task_fail': 1, 'ok': 1, 'skipped_budget': 1} | +| authoring-clickstream-claim-check | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.667 | - | 1.25 | {'ok': 2, 'skipped_budget': 1} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.958 | 0.667 | - | 0.589 | {'ok': 1, 'task_fail': 1, 'skipped_budget': 1} | +| authoring-vendor-shipments-etl | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.5 | - | 1.43 | {'ok': 2, 'skipped_budget': 1} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.714 | 0.667 | - | 0.282 | {'task_fail': 2, 'skipped_budget': 1} | +| debugging-manual-trigger-context | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.786 | 0.667 | - | 0.773 | {'task_fail': 2, 'skipped_budget': 1} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 100% | 1 | 0.833 | - | 0.444 | {'ok': 2, 'skipped_budget': 1} | +| debugging-variable-at-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0 | 0.333 | - | 0.948 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.6 | 0.167 | - | 0.46 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-inventory-context | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.8 | 0.333 | - | 1.22 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.5 | 0.667 | - | 0.749 | {'turn_limit': 2, 'skipped_budget': 1} | +| migration-revenue-previous-day | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.714 | 0.667 | - | 2.33 | {'turn_limit': 1, 'task_fail': 1, 'skipped_budget': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.357 | 0.167 | - | 0.76 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-weekly-catchup | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.714 | 0.833 | - | 2.92 | {'turn_limit': 2, 'skipped_budget': 1} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.75 | 0.833 | - | 0.89 | {'task_fail': 1, 'ok': 1, 'skipped_budget': 1} | +| scheduling-idempotent-orders-load | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 50% | 0.667 | 0.833 | - | 1.11 | {'task_fail': 1, 'ok': 1, 'skipped_budget': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.143 | 0.667 | - | 0.595 | {'task_fail': 2, 'skipped_budget': 1} | +| scheduling-weekday-ny-business-day | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.667 | - | 0.829 | {'ok': 2, 'skipped_budget': 1} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 100% | 1 | 1 | - | 0.64 | {'ok': 2, 'skipped_budget': 1} | +| testing-airflow2-ci-suite | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 1 | - | 2.06 | {'ok': 2, 'skipped_budget': 1} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.125 | 0.875 | - | 0.959 | {'turn_limit': 2, 'skipped_budget': 1} | +| testing-custom-operator-conn | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 1 | - | 1.74 | {'ok': 2, 'skipped_budget': 1} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.562 | 0.75 | - | 0.695 | {'turn_limit': 1, 'ok': 1, 'skipped_budget': 1} | +| testing-dagbag-integrity | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 1 | - | 1.27 | {'ok': 2, 'skipped_budget': 1} | + +## holdout: by arm and model + +| arm | model | runs (valid) | pass rate | primary | secondary | trigger rate | mean tokens | mean cost | mean wall s | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 21 (14) | 14% | 0.546 | 0.762 | 0% | 3.9e+06 | 0.643 | 226 | {'ok': 2, 'turn_limit': 2, 'task_fail': 10, 'skipped_budget': 7} | +| baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 21 (14) | 36% | 0.812 | 0.667 | 0% | 2.8e+06 | 1.52 | 201 | {'ok': 5, 'task_fail': 9, 'skipped_budget': 7} | + +## holdout: per case + +| case | area | arm | model | runs (valid) | pass rate | primary | secondary | skills used | mean cost | statuses | +|---|---|---|---|---|---|---|---|---|---|---| +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.833 | 1 | - | 0.701 | {'ok': 1, 'task_fail': 1, 'skipped_budget': 1} | +| authoring-daily-revenue-yesterday | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 50% | 0.833 | 0.667 | - | 1.18 | {'ok': 1, 'task_fail': 1, 'skipped_budget': 1} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.444 | 0.833 | - | 0.854 | {'turn_limit': 2, 'skipped_budget': 1} | +| authoring-partner-feeds-mapping | authoring | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.5 | - | 1.38 | {'ok': 2, 'skipped_budget': 1} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 50% | 0.95 | 0.833 | - | 0.749 | {'ok': 1, 'task_fail': 1, 'skipped_budget': 1} | +| debugging-nondeterministic-parse | debugging | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 100% | 1 | 0.667 | - | 0.811 | {'ok': 2, 'skipped_budget': 1} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.357 | 0.333 | - | 0.648 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-hourly-pageviews-templates | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.571 | 0.5 | - | 2.22 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0 | 0.667 | - | 0.463 | {'task_fail': 2, 'skipped_budget': 1} | +| migration-shipments-assets | migration | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.571 | 0.667 | - | 2.43 | {'task_fail': 2, 'skipped_budget': 1} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.611 | 0.667 | - | 0.395 | {'task_fail': 2, 'skipped_budget': 1} | +| scheduling-backfill-pageviews | scheduling | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.833 | 0.667 | - | 0.938 | {'task_fail': 2, 'skipped_budget': 1} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-haiku-4-5@20251001 | 3 (2) | 0% | 0.625 | 1 | - | 0.688 | {'task_fail': 2, 'skipped_budget': 1} | +| testing-taskflow-transforms | testing | baseline | google-vertex-anthropic/claude-sonnet-4-6@default | 3 (2) | 0% | 0.875 | 1 | - | 1.7 | {'task_fail': 2, 'skipped_budget': 1} | + +## Failure classes (all runs) + +{'ok': 32, 'task_fail': 38, 'turn_limit': 10, 'skipped_budget': 40} + +## Sources + +- `evals/airflow/results/baseline-2026-09-30`: arm=baseline models=['google-vertex-anthropic/claude-sonnet-4-6@default', 'google-vertex-anthropic/claude-haiku-4-5@20251001'] altimate-code=0.12.2 started=2026-09-30T11:53:13+00:00 budget={'cap_usd': 80.0, 'per_run_cap_usd': 5.0, 'spent_usd': 83.57, 'stopped_early': True} diff --git a/evals/airflow/results/baseline-2026-09-30/report/results.json b/evals/airflow/results/baseline-2026-09-30/report/results.json new file mode 100644 index 0000000..43be38b --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/report/results.json @@ -0,0 +1,1256 @@ +{ + "groups": [ + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2115346.0, + "mean_tokens_input": 1054568.0, + "mean_tokens_output": 6297.5, + "mean_cost_usd": 0.3731, + "total_cost_usd": 0.7462, + "mean_wall_s": 104.5, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-airflow2-inventory-snapshot", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1252527.5, + "mean_tokens_input": 624591.0, + "mean_tokens_output": 3355.0, + "mean_cost_usd": 0.734, + "total_cost_usd": 1.468, + "mean_wall_s": 97.5, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.9286, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3080173.5, + "mean_tokens_input": 1534294.5, + "mean_tokens_output": 11707.5, + "mean_cost_usd": 0.5705, + "total_cost_usd": 1.141, + "mean_wall_s": 218.35, + "statuses": { + "task_fail": 1, + "ok": 1, + "skipped_budget": 1 + }, + "retries": 1 + }, + { + "case": "authoring-clickstream-claim-check", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2143714.5, + "mean_tokens_input": 1068672.5, + "mean_tokens_output": 6386.0, + "mean_cost_usd": 1.2546, + "total_cost_usd": 2.5092, + "mean_wall_s": 149.85, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.8333, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4479344.0, + "mean_tokens_input": 2235626.5, + "mean_tokens_output": 8266.5, + "mean_cost_usd": 0.7014, + "total_cost_usd": 1.4028, + "mean_wall_s": 233.35, + "statuses": { + "ok": 1, + "task_fail": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-daily-revenue-yesterday", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.8333, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1774569.5, + "mean_tokens_input": 884705.0, + "mean_tokens_output": 5173.0, + "mean_cost_usd": 1.1801, + "total_cost_usd": 2.3602, + "mean_wall_s": 122.65, + "statuses": { + "ok": 1, + "task_fail": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.4444, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5772200.0, + "mean_tokens_input": 2876044.5, + "mean_tokens_output": 20336.0, + "mean_cost_usd": 0.8538, + "total_cost_usd": 1.7077, + "mean_wall_s": 300.75, + "statuses": { + "turn_limit": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-partner-feeds-mapping", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2035310.0, + "mean_tokens_input": 1014415.0, + "mean_tokens_output": 6495.5, + "mean_cost_usd": 1.3775, + "total_cost_usd": 2.755, + "mean_wall_s": 164.95, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.9583, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4414090.0, + "mean_tokens_input": 2200336.5, + "mean_tokens_output": 13590.0, + "mean_cost_usd": 0.589, + "total_cost_usd": 1.1779, + "mean_wall_s": 215.55, + "statuses": { + "ok": 1, + "task_fail": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "authoring-vendor-shipments-etl", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "authoring", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.5, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3092601.0, + "mean_tokens_input": 1543025.5, + "mean_tokens_output": 6572.5, + "mean_cost_usd": 1.431, + "total_cost_usd": 2.8619, + "mean_wall_s": 184.15, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1416789.5, + "mean_tokens_input": 706619.0, + "mean_tokens_output": 3606.0, + "mean_cost_usd": 0.2819, + "total_cost_usd": 0.5639, + "mean_wall_s": 74.45, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-manual-trigger-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.7857, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1575529.5, + "mean_tokens_input": 785493.0, + "mean_tokens_output": 4556.0, + "mean_cost_usd": 0.773, + "total_cost_usd": 1.546, + "mean_wall_s": 112.8, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.95, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3401602.5, + "mean_tokens_input": 1694813.0, + "mean_tokens_output": 12108.0, + "mean_cost_usd": 0.7488, + "total_cost_usd": 1.4976, + "mean_wall_s": 195.15, + "statuses": { + "ok": 1, + "task_fail": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-nondeterministic-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1144819.0, + "mean_tokens_input": 570500.5, + "mean_tokens_output": 3827.5, + "mean_cost_usd": 0.811, + "total_cost_usd": 1.622, + "mean_wall_s": 102.5, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2600769.0, + "mean_tokens_input": 1297275.0, + "mean_tokens_output": 6320.5, + "mean_cost_usd": 0.4441, + "total_cost_usd": 0.8881, + "mean_wall_s": 140.8, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "debugging-variable-at-parse", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "debugging", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1405742.0, + "mean_tokens_input": 701388.5, + "mean_tokens_output": 2976.5, + "mean_cost_usd": 0.948, + "total_cost_usd": 1.896, + "mean_wall_s": 89.5, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.3571, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4113063.5, + "mean_tokens_input": 2048915.5, + "mean_tokens_output": 15389.5, + "mean_cost_usd": 0.6485, + "total_cost_usd": 1.2971, + "mean_wall_s": 231.3, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-hourly-pageviews-templates", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.5, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4249754.5, + "mean_tokens_input": 2118982.5, + "mean_tokens_output": 11819.0, + "mean_cost_usd": 2.2202, + "total_cost_usd": 4.4405, + "mean_wall_s": 285.6, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.6, + "mean_secondary_score": 0.1667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3018427.0, + "mean_tokens_input": 1503944.0, + "mean_tokens_output": 10662.5, + "mean_cost_usd": 0.4603, + "total_cost_usd": 0.9205, + "mean_wall_s": 158.9, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-inventory-context", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.8, + "mean_secondary_score": 0.3333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2494993.0, + "mean_tokens_input": 1244430.5, + "mean_tokens_output": 6149.5, + "mean_cost_usd": 1.2222, + "total_cost_usd": 2.4444, + "mean_wall_s": 154.95, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.5, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5858017.5, + "mean_tokens_input": 2920824.5, + "mean_tokens_output": 16598.0, + "mean_cost_usd": 0.7487, + "total_cost_usd": 1.4973, + "mean_wall_s": 300.95, + "statuses": { + "turn_limit": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-revenue-previous-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5869841.0, + "mean_tokens_input": 2928927.5, + "mean_tokens_output": 12025.0, + "mean_cost_usd": 2.329, + "total_cost_usd": 4.6581, + "mean_wall_s": 325.85, + "statuses": { + "turn_limit": 1, + "task_fail": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3654380.0, + "mean_tokens_input": 1819428.0, + "mean_tokens_output": 15666.5, + "mean_cost_usd": 0.4629, + "total_cost_usd": 0.9259, + "mean_wall_s": 216.25, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-shipments-assets", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.5714, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5541317.5, + "mean_tokens_input": 2764128.5, + "mean_tokens_output": 13096.5, + "mean_cost_usd": 2.4318, + "total_cost_usd": 4.8636, + "mean_wall_s": 341.05, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.3571, + "mean_secondary_score": 0.1667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5116828.0, + "mean_tokens_input": 2551793.0, + "mean_tokens_output": 13445.0, + "mean_cost_usd": 0.76, + "total_cost_usd": 1.52, + "mean_wall_s": 256.55, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "migration-weekly-catchup", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "migration", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.7143, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 6127716.5, + "mean_tokens_input": 3058267.0, + "mean_tokens_output": 11223.5, + "mean_cost_usd": 2.9187, + "total_cost_usd": 5.8373, + "mean_wall_s": 364.1, + "statuses": { + "turn_limit": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.6111, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1990823.0, + "mean_tokens_input": 991262.0, + "mean_tokens_output": 8376.0, + "mean_cost_usd": 0.3948, + "total_cost_usd": 0.7895, + "mean_wall_s": 129.85, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-backfill-pageviews", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.8333, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1586186.5, + "mean_tokens_input": 791108.5, + "mean_tokens_output": 3981.0, + "mean_cost_usd": 0.9376, + "total_cost_usd": 1.8752, + "mean_wall_s": 109.65, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.75, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4580394.0, + "mean_tokens_input": 2278318.5, + "mean_tokens_output": 23934.5, + "mean_cost_usd": 0.8902, + "total_cost_usd": 1.7805, + "mean_wall_s": 296.65, + "statuses": { + "task_fail": 1, + "ok": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-idempotent-orders-load", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.6667, + "mean_secondary_score": 0.8333, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1933891.0, + "mean_tokens_input": 964257.0, + "mean_tokens_output": 5391.5, + "mean_cost_usd": 1.1141, + "total_cost_usd": 2.2283, + "mean_wall_s": 154.95, + "statuses": { + "task_fail": 1, + "ok": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.1429, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4234930.5, + "mean_tokens_input": 2110562.5, + "mean_tokens_output": 13967.0, + "mean_cost_usd": 0.5949, + "total_cost_usd": 1.1899, + "mean_wall_s": 202.2, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "scheduling-weekday-ny-business-day", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "scheduling", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 1293609.0, + "mean_tokens_input": 645068.5, + "mean_tokens_output": 3482.5, + "mean_cost_usd": 0.8291, + "total_cost_usd": 1.6581, + "mean_wall_s": 95.55, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4616548.5, + "mean_tokens_input": 2300068.0, + "mean_tokens_output": 16579.5, + "mean_cost_usd": 0.6399, + "total_cost_usd": 1.2798, + "mean_wall_s": 234.35, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-airflow2-ci-suite", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "2.11", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 4206678.5, + "mean_tokens_input": 2096082.5, + "mean_tokens_output": 14542.0, + "mean_cost_usd": 2.0604, + "total_cost_usd": 4.1207, + "mean_wall_s": 302.75, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.125, + "mean_secondary_score": 0.875, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5855954.5, + "mean_tokens_input": 2918620.5, + "mean_tokens_output": 18936.0, + "mean_cost_usd": 0.9594, + "total_cost_usd": 1.9187, + "mean_wall_s": 288.65, + "statuses": { + "turn_limit": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-custom-operator-conn", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3199189.5, + "mean_tokens_input": 1594765.5, + "mean_tokens_output": 9682.0, + "mean_cost_usd": 1.7393, + "total_cost_usd": 3.4786, + "mean_wall_s": 240.45, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.5, + "mean_primary_score": 0.5625, + "mean_secondary_score": 0.75, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 5008500.0, + "mean_tokens_input": 2498426.0, + "mean_tokens_output": 11838.0, + "mean_cost_usd": 0.6948, + "total_cost_usd": 1.3897, + "mean_wall_s": 248.4, + "statuses": { + "turn_limit": 1, + "ok": 1, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-dagbag-integrity", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "dev", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 1.0, + "mean_primary_score": 1.0, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2188061.0, + "mean_tokens_input": 1090850.5, + "mean_tokens_output": 6376.5, + "mean_cost_usd": 1.2672, + "total_cost_usd": 2.5345, + "mean_wall_s": 173.0, + "statuses": { + "ok": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.625, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3893233.5, + "mean_tokens_input": 1934544.5, + "mean_tokens_output": 24287.5, + "mean_cost_usd": 0.6876, + "total_cost_usd": 1.3752, + "mean_wall_s": 275.45, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + }, + { + "case": "testing-taskflow-transforms", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "split": "holdout", + "area": "testing", + "airflow_version": "3.3", + "n_runs": 3, + "n_valid": 2, + "n_excluded": 1, + "pass_rate": 0.0, + "mean_primary_score": 0.875, + "mean_secondary_score": 1.0, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3298667.5, + "mean_tokens_input": 1642369.0, + "mean_tokens_output": 13953.0, + "mean_cost_usd": 1.7015, + "total_cost_usd": 3.403, + "mean_wall_s": 281.45, + "statuses": { + "task_fail": 2, + "skipped_budget": 1 + }, + "retries": 0 + } + ], + "by_split_arm_model": [ + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 39, + "n_valid": 26, + "n_excluded": 13, + "pass_rate": 0.3846, + "mean_primary_score": 0.6645, + "mean_secondary_score": 0.6763, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3993597.5385, + "mean_tokens_input": 1990434.6154, + "mean_tokens_output": 12883.2308, + "mean_cost_usd": 0.6159, + "total_cost_usd": 16.0136, + "mean_wall_s": 210.7923, + "statuses": { + "ok": 10, + "task_fail": 11, + "turn_limit": 5, + "skipped_budget": 13 + }, + "retries": 1 + }, + { + "split": "dev", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 39, + "n_valid": 26, + "n_excluded": 13, + "pass_rate": 0.5769, + "mean_primary_score": 0.8216, + "mean_secondary_score": 0.7051, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2829545.6923, + "mean_tokens_input": 1411216.8846, + "mean_tokens_output": 7132.1923, + "mean_cost_usd": 1.4324, + "total_cost_usd": 37.2412, + "mean_wall_s": 188.1077, + "statuses": { + "ok": 15, + "task_fail": 8, + "turn_limit": 3, + "skipped_budget": 13 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", + "n_runs": 21, + "n_valid": 14, + "n_excluded": 7, + "pass_rate": 0.1429, + "mean_primary_score": 0.5459, + "mean_secondary_score": 0.7619, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 3900663.7857, + "mean_tokens_input": 1942947.7143, + "mean_tokens_output": 14918.5714, + "mean_cost_usd": 0.6426, + "total_cost_usd": 8.9957, + "mean_wall_s": 226.0143, + "statuses": { + "ok": 2, + "turn_limit": 2, + "task_fail": 10, + "skipped_budget": 7 + }, + "retries": 0 + }, + { + "split": "holdout", + "arm": "baseline", + "model": "google-vertex-anthropic/claude-sonnet-4-6@default", + "n_runs": 21, + "n_valid": 14, + "n_excluded": 7, + "pass_rate": 0.3571, + "mean_primary_score": 0.8121, + "mean_secondary_score": 0.6667, + "trigger_rate": 0.0, + "skills_used": {}, + "mean_tokens_total": 2804374.9286, + "mean_tokens_input": 1398029.8571, + "mean_tokens_output": 8335.0714, + "mean_cost_usd": 1.5228, + "total_cost_usd": 21.3194, + "mean_wall_s": 201.1214, + "statuses": { + "ok": 5, + "task_fail": 9, + "skipped_budget": 7 + }, + "retries": 0 + } + ], + "paired_deltas": [], + "failure_classes": { + "ok": 32, + "task_fail": 38, + "turn_limit": 10, + "skipped_budget": 40 + }, + "sources": [ + { + "dir": "evals/airflow/results/baseline-2026-09-30", + "run_name": "baseline-2026-09-30", + "arm": "baseline", + "models": [ + "google-vertex-anthropic/claude-sonnet-4-6@default", + "google-vertex-anthropic/claude-haiku-4-5@20251001" + ], + "split": "all", + "started_at": "2026-09-30T11:53:13+00:00", + "finished_at": "2026-09-30T13:20:21+00:00", + "altimate_code_version": "0.12.2", + "envs": { + "2.11": { + "python": "3.12.8", + "airflow": "2.11.2", + "pytest": "9.1.1", + "duckdb": "1.4.4", + "pandas": "2.1.4", + "ruff": "0.16.9", + "python_path": "~/.cache/des-evals/airflow-2.11/bin/python" + }, + "3.3": { + "python": "3.12.8", + "airflow": "3.3.2", + "pytest": "9.1.1", + "duckdb": "1.5.6", + "pandas": "3.0.5", + "ruff": "0.16.9", + "python_path": "~/.cache/des-evals/airflow-3.3/bin/python" + } + }, + "airflow_skills_sha256": null, + "staged_skills_sha256": "ca2b482d4ec517502521a5686cd32137c1da615ed31db7a7fa543f031b4050f0", + "harness_sha256": "8a34b5a45a17fc7b278e59e1fcc16d6a65419e7865d4eda84d04734f28478eae", + "repo_head": "705c68b706ffdd667e7f205af2cacac655806669", + "budget": { + "cap_usd": 80.0, + "per_run_cap_usd": 5.0, + "spent_usd": 83.57, + "stopped_early": true + }, + "status_counts": { + "ok": 32, + "skipped_budget": 40, + "task_fail": 38, + "turn_limit": 10 + } + } + ] +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs.jsonl b/evals/airflow/results/baseline-2026-09-30/runs.jsonl new file mode 100644 index 0000000..96977d4 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs.jsonl @@ -0,0 +1,120 @@ +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 90.5, "cost_usd": 0.719271, "cost_reported_usd": 2.477547, "tokens": {"input": 586101, "output": 3114, "reasoning": 0, "cache_read": 442119, "cache_write": 143973, "total": 1175307}, "steps": 8, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 2, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65c1fffeLY05DTRQcSxPLb.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.719271, "cost_reported_usd": 2.477547, "tokens": {"input": 586101, "output": 3114, "reasoning": 0, "cache_read": 442119, "cache_write": 143973, "total": 1175307}, "wall_s": 90.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 100.5, "cost_usd": 0.284432, "cost_reported_usd": 1.23052, "tokens": {"input": 946165, "output": 6420, "reasoning": 0, "cache_read": 809004, "cache_write": 137084, "total": 1898673}, "steps": 14, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 10, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd66033ffeCN3SvKAbT0C4br.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.284432, "cost_reported_usd": 1.23052, "tokens": {"input": 946165, "output": 6420, "reasoning": 0, "cache_read": 809004, "cache_write": 137084, "total": 1898673}, "wall_s": 100.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 124.6, "cost_usd": 0.751673, "cost_reported_usd": 2.77565, "tokens": {"input": 674670, "output": 5275, "reasoning": 0, "cache_read": 538393, "cache_write": 136266, "total": 1354604}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65e3affezdP3xxVk0Ni2ey.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.751673, "cost_reported_usd": 2.77565, "tokens": {"input": 674670, "output": 5275, "reasoning": 0, "cache_read": 538393, "cache_write": 136266, "total": 1354604}, "wall_s": 124.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 126.7, "cost_usd": 1.243521, "cost_reported_usd": 3.492666, "tokens": {"input": 749727, "output": 5462, "reasoning": 0, "cache_read": 478225, "cache_write": 271490, "total": 1504904}, "steps": 11, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd4b83effezxyqVZ55IsPvt5.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 1.243521, "cost_reported_usd": 3.492666, "tokens": {"input": 749727, "output": 5462, "reasoning": 0, "cache_read": 478225, "cache_write": 271490, "total": 1504904}, "wall_s": 126.7, "airflow_skills_used": [], "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: no XCom/Variable value larger than 48 KB (data passed by reference)", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 1.0, "returncode": 0, "wall_s": 229.4, "cost_usd": 0.57126, "cost_reported_usd": 2.240903, "tokens": {"input": 1669775, "output": 7936, "reasoning": 0, "cache_read": 1352701, "cache_write": 316942, "total": 3347354}, "steps": 25, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 17, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65c1bffeZXPZNvOGJu2TNM.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 1.0, "cost_usd": 0.57126, "cost_reported_usd": 2.240903, "tokens": {"input": 1669775, "output": 7936, "reasoning": 0, "cache_read": 1352701, "cache_write": 316942, "total": 3347354}, "wall_s": 229.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 231.2, "cost_usd": 0.66357, "cost_reported_usd": 2.568192, "tokens": {"input": 1904772, "output": 7621, "reasoning": 0, "cache_read": 1526489, "cache_write": 378133, "total": 3817015}, "steps": 26, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 19, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd48961ffeeIiesd1Z6L8z43.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.66357, "cost_reported_usd": 2.568192, "tokens": {"input": 1904772, "output": 7621, "reasoning": 0, "cache_read": 1526489, "cache_write": 378133, "total": 3817015}, "wall_s": 231.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 197.1, "cost_usd": 1.718919, "cost_reported_usd": 5.543784, "tokens": {"input": 1274974, "output": 7651, "reasoning": 0, "cache_read": 920865, "cache_write": 354090, "total": 2557580}, "steps": 18, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 12, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd3c439ffewlelwExv5Te0TD.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 1.718919, "cost_reported_usd": 5.543784, "tokens": {"input": 1274974, "output": 7651, "reasoning": 0, "cache_read": 920865, "cache_write": 354090, "total": 2557580}, "wall_s": 197.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 100.8, "cost_usd": 0.728931, "cost_reported_usd": 2.764158, "tokens": {"input": 678420, "output": 3932, "reasoning": 0, "cache_read": 543222, "cache_write": 135187, "total": 1360761}, "steps": 10, "termination": {"why_model_stopped": "stop", "why_harness_stopped": "none", "done_reason": "none"}, "skill_invocations": [{"name": "debugging-dbt-errors", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcfb246ffeoZuqc6334rQq2y.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "cost_usd": 0.728931, "cost_reported_usd": 2.764158, "tokens": {"input": 678420, "output": 3932, "reasoning": 0, "cache_read": 543222, "cache_write": 135187, "total": 1360761}, "wall_s": 100.8, "airflow_skills_used": [], "skill_invocations": [{"name": "debugging-dbt-errors", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.0, "secondary_score": 1.0, "returncode": 1, "wall_s": 311.8, "cost_usd": 0.855348, "cost_reported_usd": 3.721205, "tokens": {"input": 2866080, "output": 20856, "reasoning": 0, "cache_read": 2462153, "cache_write": 403704, "total": 5752793}, "steps": 42, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 23, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd22c15ffeKWRQKQlDt21HTv.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.0, "secondary_score": 1.0, "cost_usd": 0.855348, "cost_reported_usd": 3.721205, "tokens": {"input": 2866080, "output": 20856, "reasoning": 0, "cache_read": 2462153, "cache_write": 403704, "total": 5752793}, "wall_s": 311.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 84.5, "cost_usd": 0.332797, "cost_reported_usd": 1.138662, "tokens": {"input": 805929, "output": 4215, "reasoning": 0, "cache_read": 604933, "cache_write": 200932, "total": 1616009}, "steps": 12, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 7, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcd84d0ffeUr3arKaVLLAd0D.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "cost_usd": 0.332797, "cost_reported_usd": 1.138662, "tokens": {"input": 805929, "output": 4215, "reasoning": 0, "cache_read": 604933, "cache_write": 200932, "total": 1616009}, "wall_s": 84.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 104.5, "cost_usd": 0.689099, "cost_reported_usd": 2.299124, "tokens": {"input": 536684, "output": 4199, "reasoning": 0, "cache_read": 401868, "cache_write": 134807, "total": 1077558}, "steps": 8, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcd5a69ffe20nymQ7HlFmHV3.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.689099, "cost_reported_usd": 2.299124, "tokens": {"input": 536684, "output": 4199, "reasoning": 0, "cache_read": 401868, "cache_write": 134807, "total": 1077558}, "wall_s": 104.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 245.5, "cost_usd": 1.811411, "cost_reported_usd": 8.585213, "tokens": {"input": 2257966, "output": 8248, "reasoning": 0, "cache_read": 1965118, "cache_write": 292816, "total": 4524148}, "steps": 31, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 21, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd1cc26ffe8bHWSJ4q3JvFSw.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 1.811411, "cost_reported_usd": 8.585213, "tokens": {"input": 2257966, "output": 8248, "reasoning": 0, "cache_read": 1965118, "cache_write": 292816, "total": 4524148}, "wall_s": 245.5, "airflow_skills_used": [], "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: DAG files import with no Variables available, DAG warehouse_load is present", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 80.4, "cost_usd": 0.929949, "cost_reported_usd": 2.929881, "tokens": {"input": 666655, "output": 2501, "reasoning": 0, "cache_read": 465946, "cache_write": 200698, "total": 1335800}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcb41f7ffexZbVg1SwxSAaut.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.3333333333333333, "cost_usd": 0.929949, "cost_reported_usd": 2.929881, "tokens": {"input": 666655, "output": 2501, "reasoning": 0, "cache_read": 465946, "cache_write": 200698, "total": 1335800}, "wall_s": 80.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 266.1, "cost_usd": 0.700714, "cost_reported_usd": 3.2588, "tokens": {"input": 2558286, "output": 17984, "reasoning": 0, "cache_read": 2249577, "cache_write": 308509, "total": 5134356}, "steps": 37, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 24, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd064acffe0h7WXKewiO8lhK.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.700714, "cost_reported_usd": 3.2588, "tokens": {"input": 2558286, "output": 17984, "reasoning": 0, "cache_read": 2249577, "cache_write": 308509, "total": 5134356}, "wall_s": 266.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 219.3, "cost_usd": 0.759712, "cost_reported_usd": 2.441651, "tokens": {"input": 1682068, "output": 13616, "reasoning": 0, "cache_read": 1226888, "cache_write": 455051, "total": 3377623}, "steps": 25, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 14, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcbb3fdffeWjKSoRy60SIs7L.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.759712, "cost_reported_usd": 2.441651, "tokens": {"input": 1682068, "output": 13616, "reasoning": 0, "cache_read": 1226888, "cache_write": 455051, "total": 3377623}, "wall_s": 219.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 211.2, "cost_usd": 0.562921, "cost_reported_usd": 2.41603, "tokens": {"input": 1853255, "output": 9299, "reasoning": 0, "cache_read": 1565310, "cache_write": 287799, "total": 3715663}, "steps": 26, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 18, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcaaa54ffepttK6P4Xsc1QV7.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.562921, "cost_reported_usd": 2.41603, "tokens": {"input": 1853255, "output": 9299, "reasoning": 0, "cache_read": 1565310, "cache_write": 287799, "total": 3715663}, "wall_s": 211.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: dags test succeeds, consecutive scheduled daily runs succeed on 3.3, snapshots and audit records match the Airflow 2 output", "primary_pass": false, "primary_score": 0.4, "secondary_score": 0.0, "returncode": 0, "wall_s": 70.4, "cost_usd": 0.233456, "cost_reported_usd": 0.903006, "tokens": {"input": 669606, "output": 2592, "reasoning": 0, "cache_read": 536085, "cache_write": 133465, "total": 1341748}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 5, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc6fd65ffegrL6YTwHYdSJ7u.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.4, "secondary_score": 0.0, "cost_usd": 0.233456, "cost_reported_usd": 0.903006, "tokens": {"input": 669606, "output": 2592, "reasoning": 0, "cache_read": 536085, "cache_write": 133465, "total": 1341748}, "wall_s": 70.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: hourly rollups (previous clock hour, change vs hour before) match 2.11, completion markers match 2.11, staging cleanup keeps and deletes the same files as 2.11", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 285.6, "cost_usd": 2.086828, "cost_reported_usd": 8.151403, "tokens": {"input": 2021553, "output": 12143, "reasoning": 0, "cache_read": 1645252, "cache_write": 376273, "total": 4055221}, "steps": 27, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 26, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc9e333ffeSSU076Npto2DiV.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.3333333333333333, "cost_usd": 2.086828, "cost_reported_usd": 8.151403, "tokens": {"input": 2021553, "output": 12143, "reasoning": 0, "cache_read": 1645252, "cache_write": 376273, "total": 4055221}, "wall_s": 285.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: hourly rollups (previous clock hour, change vs hour before) match 2.11, completion markers match 2.11, staging cleanup keeps and deletes the same files as 2.11", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 251.4, "cost_usd": 0.700514, "cost_reported_usd": 3.20054, "tokens": {"input": 2500217, "output": 15619, "reasoning": 0, "cache_read": 2176352, "cache_write": 323674, "total": 5015862}, "steps": 36, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 19, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc90aa8ffe7GCspXON7RI74a.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.3333333333333333, "cost_usd": 0.700514, "cost_reported_usd": 3.20054, "tokens": {"input": 2500217, "output": 15619, "reasoning": 0, "cache_read": 2176352, "cache_write": 323674, "total": 5015862}, "wall_s": 251.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: snapshots and audit records match the Airflow 2 output", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 183.0, "cost_usd": 1.382806, "cost_reported_usd": 5.988592, "tokens": {"input": 1535282, "output": 7256, "reasoning": 0, "cache_read": 1299515, "cache_write": 235747, "total": 3077800}, "steps": 19, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 15, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc7c8d0ffeUBxxZ833txsFJ8.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.6666666666666666, "cost_usd": 1.382806, "cost_reported_usd": 5.988592, "tokens": {"input": 1535282, "output": 7256, "reasoning": 0, "cache_read": 1299515, "cache_write": 235747, "total": 3077800}, "wall_s": 183.0, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: DAGs import cleanly on Airflow 3.3, both DAGs keep their tasks and dependencies, carrier_scorecard is still scheduled on the asset ingest_shipments updates, dags test succeeds (ingest_shipments, carrier_scorecard)", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 213.3, "cost_usd": 0.450853, "cost_reported_usd": 2.18923, "tokens": {"input": 1738508, "output": 15464, "reasoning": 0, "cache_read": 1564843, "cache_write": 173534, "total": 3492349}, "steps": 26, "termination": {"why_model_stopped": "stop", "why_harness_stopped": "none", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 11, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc449feffebH5mLNQEqcsfdM.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.450853, "cost_reported_usd": 2.18923, "tokens": {"input": 1738508, "output": 15464, "reasoning": 0, "cache_read": 1564843, "cache_write": 173534, "total": 3492349}, "wall_s": 213.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "returncode": 1, "wall_s": 300.1, "cost_usd": 0.734269, "cost_reported_usd": 3.628339, "tokens": {"input": 2894300, "output": 16158, "reasoning": 0, "cache_read": 2577686, "cache_write": 316384, "total": 5804528}, "steps": 42, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 25, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc4d559ffeQoIsQ2bW0gh5jj.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "cost_usd": 0.734269, "cost_reported_usd": 3.628339, "tokens": {"input": 2894300, "output": 16158, "reasoning": 0, "cache_read": 2577686, "cache_write": 316384, "total": 5804528}, "wall_s": 300.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "returncode": 1, "wall_s": 342.0, "cost_usd": 2.418337, "cost_reported_usd": 11.915701, "tokens": {"input": 3165829, "output": 12830, "reasoning": 0, "cache_read": 2795925, "cache_write": 369863, "total": 6344447}, "steps": 40, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [{"name": "altimate-code", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 34, "edited_files": 5, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc57311ffeu7ym49DXcMSf6P.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "cost_usd": 2.418337, "cost_reported_usd": 11.915701, "tokens": {"input": 3165829, "output": 12830, "reasoning": 0, "cache_read": 2795925, "cache_write": 369863, "total": 6344447}, "wall_s": 342.0, "airflow_skills_used": [], "skill_invocations": [{"name": "altimate-code", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: three hourly loads and the scorecard run succeed on 3.3, every carrier event is loaded exactly once, in the same batches as 2.11, scorecard and _READY marker match 2.11", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 326.2, "cost_usd": 2.070052, "cost_reported_usd": 10.508515, "tokens": {"input": 2812859, "output": 11839, "reasoning": 0, "cache_read": 2508906, "cache_write": 303915, "total": 5637519}, "steps": 37, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 36, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc486a8ffeFP9Wzm7wPAxpSp.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "cost_usd": 2.070052, "cost_reported_usd": 10.508515, "tokens": {"input": 2812859, "output": 11839, "reasoning": 0, "cache_read": 2508906, "cache_write": 303915, "total": 5637519}, "wall_s": 326.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: each run loads a day whose export has already landed", "primary_pass": false, "primary_score": 0.8888888888888888, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 110.6, "cost_usd": 1.05426, "cost_reported_usd": 3.107331, "tokens": {"input": 684367, "output": 4219, "reasoning": 0, "cache_read": 456636, "cache_write": 227721, "total": 1372943}, "steps": 9, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dbf1660ffeh7kp7HoVrMFCFj.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8888888888888888, "secondary_score": 0.6666666666666666, "cost_usd": 1.05426, "cost_reported_usd": 3.107331, "tokens": {"input": 684367, "output": 4219, "reasoning": 0, "cache_read": 456636, "cache_write": 227721, "total": 1372943}, "wall_s": 110.6, "airflow_skills_used": [], "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 1.0, "returncode": 1, "wall_s": 384.2, "cost_usd": 2.618652, "cost_reported_usd": 11.76945, "tokens": {"input": 3050307, "output": 10299, "reasoning": 0, "cache_read": 2601291, "cache_write": 448975, "total": 6110872}, "steps": 40, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 31, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dc0c0f8ffeQe5uxGoYFLHG0N.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 1.0, "cost_usd": 2.618652, "cost_reported_usd": 11.76945, "tokens": {"input": 3050307, "output": 10299, "reasoning": 0, "cache_read": 2601291, "cache_write": 448975, "total": 6110872}, "wall_s": 384.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: dags test succeeds (weekly_campaign_report, campaign_sync), scheduled Monday 06:00 runs succeed on 3.3, Monday runs report on the previous week, same output as 2.11, season backfill: scheduler creates the same weekly runs from start_date as 2.11", "primary_pass": false, "primary_score": 0.42857142857142855, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 297.8, "cost_usd": 0.81076, "cost_reported_usd": 3.692652, "tokens": {"input": 2882120, "output": 14273, "reasoning": 0, "cache_read": 2489737, "cache_write": 392155, "total": 5778285}, "steps": 41, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 25, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dbf4c08ffe6Goue5AX3R8Lnq.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.42857142857142855, "secondary_score": 0.3333333333333333, "cost_usd": 0.81076, "cost_reported_usd": 3.692652, "tokens": {"input": 2882120, "output": 14273, "reasoning": 0, "cache_read": 2489737, "cache_write": 392155, "total": 5778285}, "wall_s": 297.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: airflow dags test succeeds, scheduled runs load exactly their own day, re-running a day creates no duplicates and keeps the previous day, re-run repairs a partially written day", "primary_pass": false, "primary_score": 0.3333333333333333, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 132.8, "cost_usd": 0.768897, "cost_reported_usd": 2.867745, "tokens": {"input": 699627, "output": 5195, "reasoning": 0, "cache_read": 560180, "cache_write": 139436, "total": 1404438}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dbc44f2ffeWhItYs0RyCI9av.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.3333333333333333, "secondary_score": 0.6666666666666666, "cost_usd": 0.768897, "cost_reported_usd": 2.867745, "tokens": {"input": 699627, "output": 5195, "reasoning": 0, "cache_read": 560180, "cache_write": 139436, "total": 1404438}, "wall_s": 132.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 90.5, "cost_usd": 0.937392, "cost_reported_usd": 2.802252, "tokens": {"input": 621630, "output": 3114, "reasoning": 0, "cache_read": 417514, "cache_write": 204106, "total": 1246364}, "steps": 9, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db9f63dffeJiiNmNgD2HmJIk.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.937392, "cost_reported_usd": 2.802252, "tokens": {"input": 621630, "output": 3114, "reasoning": 0, "cache_read": 417514, "cache_write": 204106, "total": 1246364}, "wall_s": 90.5, "airflow_skills_used": [], "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: DAGs import cleanly, DAG fx_pnl_report exists", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 189.1, "cost_usd": 0.525243, "cost_reported_usd": 2.323865, "tokens": {"input": 1798761, "output": 14046, "reasoning": 0, "cache_read": 1559481, "cache_write": 239141, "total": 3611429}, "steps": 27, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 12, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db9c420ffeYfNuJ1h6jVRDab.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.525243, "cost_reported_usd": 2.323865, "tokens": {"input": 1798761, "output": 14046, "reasoning": 0, "cache_read": 1559481, "cache_write": 239141, "total": 3611429}, "wall_s": 189.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: one writer at a time, catchup backfills exactly 09-01..09-29, each run loads a day whose export has already landed, re-running a day repairs it and keeps other days, airflow dags test succeeds", "primary_pass": false, "primary_score": 0.4444444444444444, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 189.3, "cost_usd": 0.471786, "cost_reported_usd": 1.787929, "tokens": {"input": 1316245, "output": 13253, "reasoning": 0, "cache_read": 1078052, "cache_write": 238091, "total": 2645641}, "steps": 20, "termination": {"why_model_stopped": "stop", "why_harness_stopped": "none", "done_reason": "none"}, "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 10, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dbe1eb7ffex7xUpgOhmZbyUm.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.4444444444444444, "secondary_score": 0.6666666666666666, "cost_usd": 0.471786, "cost_reported_usd": 1.787929, "tokens": {"input": 1316245, "output": 13253, "reasoning": 0, "cache_read": 1078052, "cache_write": 238091, "total": 2645641}, "wall_s": 189.3, "airflow_skills_used": [], "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: scheduled runs load exactly their own day, re-running a day creates no duplicates and keeps the previous day, re-run repairs a partially written day", "primary_pass": false, "primary_score": 0.5, "secondary_score": 1.0, "returncode": 0, "wall_s": 255.3, "cost_usd": 0.862937, "cost_reported_usd": 3.347052, "tokens": {"input": 2484307, "output": 18774, "reasoning": 0, "cache_read": 2031538, "cache_write": 452577, "total": 4987196}, "steps": 36, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 19, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dba1a4effe12U43ufgINrC13.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5, "secondary_score": 1.0, "cost_usd": 0.862937, "cost_reported_usd": 3.347052, "tokens": {"input": 2484307, "output": 18774, "reasoning": 0, "cache_read": 2031538, "cache_write": 452577, "total": 4987196}, "wall_s": 255.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 229.3, "cost_usd": 0.683605, "cost_reported_usd": 3.092191, "tokens": {"input": 2408764, "output": 14924, "reasoning": 0, "cache_read": 2088631, "cache_write": 319955, "total": 4832274}, "steps": 35, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "idle-done", "done_reason": "idle_heuristic"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 16, "edited_files": 8, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db6cc8affeKoJC7167f6Uex9.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.683605, "cost_reported_usd": 3.092191, "tokens": {"input": 2408764, "output": 14924, "reasoning": 0, "cache_read": 2088631, "cache_write": 319955, "total": 4832274}, "wall_s": 229.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 333.9, "cost_usd": 2.662803, "cost_reported_usd": 10.092891, "tokens": {"input": 2476728, "output": 15491, "reasoning": 0, "cache_read": 1987614, "cache_write": 489082, "total": 4968915}, "steps": 31, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 24, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db7ec68ffe9HlNv6WN7MK8wO.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 2.662803, "cost_reported_usd": 10.092891, "tokens": {"input": 2476728, "output": 15491, "reasoning": 0, "cache_read": 1987614, "cache_write": 489082, "total": 4968915}, "wall_s": 333.9, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 249.4, "cost_usd": 1.90319, "cost_reported_usd": 7.009121, "tokens": {"input": 1702002, "output": 8868, "reasoning": 0, "cache_read": 1336904, "cache_write": 365073, "total": 3412847}, "steps": 24, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 15, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db63d03ffeUMHVvJEwZN5Fp7.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 1.90319, "cost_reported_usd": 7.009121, "tokens": {"input": 1702002, "output": 8868, "reasoning": 0, "cache_read": 1336904, "cache_write": 365073, "total": 3412847}, "wall_s": 249.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.125, "secondary_score": 1.0, "returncode": 1, "wall_s": 271.6, "cost_usd": 0.924261, "cost_reported_usd": 3.844329, "tokens": {"input": 2920293, "output": 16620, "reasoning": 0, "cache_read": 2442738, "cache_write": 477330, "total": 5856981}, "steps": 42, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 21, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db5a8fbfferur7TeOumrdYcw.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.125, "secondary_score": 1.0, "cost_usd": 0.924261, "cost_reported_usd": 3.844329, "tokens": {"input": 2920293, "output": 16620, "reasoning": 0, "cache_read": 2442738, "cache_write": 477330, "total": 5856981}, "wall_s": 271.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 213.2, "cost_usd": 1.743367, "cost_reported_usd": 5.939188, "tokens": {"input": 1398628, "output": 7922, "reasoning": 0, "cache_read": 1049363, "cache_write": 349244, "total": 2805157}, "steps": 20, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 15, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db30e84ffebVzPEfjO0LSCPX.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 1.743367, "cost_reported_usd": 5.939188, "tokens": {"input": 1398628, "output": 7922, "reasoning": 0, "cache_read": 1049363, "cache_write": 349244, "total": 2805157}, "wall_s": 213.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.125, "secondary_score": 0.5, "returncode": 1, "wall_s": 293.7, "cost_usd": 0.861153, "cost_reported_usd": 3.784843, "tokens": {"input": 2923913, "output": 14325, "reasoning": 0, "cache_read": 2491572, "cache_write": 432118, "total": 5861928}, "steps": 43, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 20, "edited_files": 6, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db27d59ffe0cdksy7v2G6xOe.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.125, "secondary_score": 0.5, "cost_usd": 0.861153, "cost_reported_usd": 3.784843, "tokens": {"input": 2923913, "output": 14325, "reasoning": 0, "cache_read": 2491572, "cache_write": 432118, "total": 5861928}, "wall_s": 293.7, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 104.5, "cost_usd": 0.74868, "cost_reported_usd": 2.737893, "tokens": {"input": 663081, "output": 3596, "reasoning": 0, "cache_read": 519364, "cache_write": 143707, "total": 1329748}, "steps": 9, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0daf8722ffeW70425b2VMmwmc.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.74868, "cost_reported_usd": 2.737893, "tokens": {"input": 663081, "output": 3596, "reasoning": 0, "cache_read": 519364, "cache_write": 143707, "total": 1329748}, "wall_s": 104.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: suite catches hidden bug: validation_bypassed", "primary_pass": false, "primary_score": 0.875, "secondary_score": 1.0, "returncode": 0, "wall_s": 313.6, "cost_usd": 1.812216, "cost_reported_usd": 7.30497, "tokens": {"input": 1830944, "output": 15902, "reasoning": 0, "cache_read": 1534010, "cache_write": 296908, "total": 3677764}, "steps": 25, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 20, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db20449ffeh5AeXmAmVVmVd7.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.875, "secondary_score": 1.0, "cost_usd": 1.812216, "cost_reported_usd": 7.30497, "tokens": {"input": 1830944, "output": 15902, "reasoning": 0, "cache_read": 1534010, "cache_write": 296908, "total": 3677764}, "wall_s": 313.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 1, "attempts": [{"status": "task_fail", "reason": "failed primary: suite catches hidden bug: dedupe_keeps_first, suite catches hidden bug: rounding_truncates, suite catches hidden bug: unknown_currency_as_usd, suite catches hidden bug: validation_bypassed", "primary_pass": false, "primary_score": 0.5, "secondary_score": 1.0, "returncode": 0, "wall_s": 295.7, "cost_usd": 0.848489, "cost_reported_usd": 3.283607, "tokens": {"input": 2435298, "output": 23641, "reasoning": 0, "cache_read": 2011994, "cache_write": 423124, "total": 4894057}, "steps": 35, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 11, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0db130d9ffeyUAzLRMf6nbEdJ.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5, "secondary_score": 1.0, "cost_usd": 0.848489, "cost_reported_usd": 3.283607, "tokens": {"input": 2435298, "output": 23641, "reasoning": 0, "cache_read": 2011994, "cache_write": 423124, "total": 4894057}, "wall_s": 295.7, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 108.5, "cost_usd": 0.461784, "cost_reported_usd": 1.624657, "tokens": {"input": 1162971, "output": 6175, "reasoning": 0, "cache_read": 889374, "cache_write": 273499, "total": 2332019}, "steps": 17, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 13, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dada241ffe7NBnmWGHbI3lJ1.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.461784, "cost_reported_usd": 1.624657, "tokens": {"input": 1162971, "output": 6175, "reasoning": 0, "cache_read": 889374, "cache_write": 273499, "total": 2332019}, "wall_s": 108.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 175.1, "cost_usd": 1.75752, "cost_reported_usd": 6.145479, "tokens": {"input": 1462675, "output": 7497, "reasoning": 0, "cache_read": 1113029, "cache_write": 349624, "total": 2932825}, "steps": 21, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 9, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dad7bfbffeMEfR23VO4o3WkS.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 1.75752, "cost_reported_usd": 6.145479, "tokens": {"input": 1462675, "output": 7497, "reasoning": 0, "cache_read": 1113029, "cache_write": 349624, "total": 2932825}, "wall_s": 175.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: the 03:00 run on day D reports calendar day D-1 (and only that day), rerun of a scheduled run reproduces the same file", "primary_pass": false, "primary_score": 0.6666666666666666, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 118.6, "cost_usd": 1.116698, "cost_reported_usd": 4.175702, "tokens": {"input": 1019683, "output": 4884, "reasoning": 0, "cache_read": 805902, "cache_write": 213766, "total": 2044235}, "steps": 14, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 6, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dac6b9bffex6LTTutrtExlrr.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.6666666666666666, "secondary_score": 0.6666666666666666, "cost_usd": 1.116698, "cost_reported_usd": 4.175702, "tokens": {"input": 1019683, "output": 4884, "reasoning": 0, "cache_read": 805902, "cache_write": 213766, "total": 2044235}, "wall_s": 118.6, "airflow_skills_used": [], "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "infra_error", "reason": "no model steps completed: \u001b[91m\u001b[1mError: \u001b[0mModel not found: google-vertex-anthropic/claude-haiku-4-5@20251001\nTry: `altimate models` to list available models\nOr check your config (altimate-code.json) provider/model names", "primary_pass": false, "primary_score": null, "secondary_score": null, "returncode": 1, "wall_s": 8.0, "cost_usd": 0.0, "cost_reported_usd": 0.0, "tokens": {"input": 0, "output": 0, "reasoning": 0, "cache_read": 0, "cache_write": 0, "total": 0}, "steps": 0, "termination": {}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 0, "edited_files": 0, "errors": [], "trace_path": null, "attempt": 0}, {"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 207.3, "cost_usd": 0.569764, "cost_reported_usd": 1.968464, "tokens": {"input": 1398814, "output": 15479, "reasoning": 0, "cache_read": 1092278, "cache_write": 306422, "total": 2812993}, "steps": 21, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 21, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dac80a6ffeAxP2Jhf6Esly9U.json", "attempt": 1}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.569764, "cost_reported_usd": 1.968464, "tokens": {"input": 1398814, "output": 15479, "reasoning": 0, "cache_read": 1092278, "cache_write": 306422, "total": 2812993}, "wall_s": 207.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 1} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: the 03:00 run on day D reports calendar day D-1 (and only that day), rerun of a scheduled run reproduces the same file", "primary_pass": false, "primary_score": 0.6666666666666666, "secondary_score": 1.0, "returncode": 0, "wall_s": 235.5, "cost_usd": 0.739199, "cost_reported_usd": 3.305479, "tokens": {"input": 2566481, "output": 8912, "reasoning": 0, "cache_read": 2185576, "cache_write": 380704, "total": 5141673}, "steps": 35, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 28, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0dabbb40ffeQZel2S2G84JvSB.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.6666666666666666, "secondary_score": 1.0, "cost_usd": 0.739199, "cost_reported_usd": 3.305479, "tokens": {"input": 2566481, "output": 8912, "reasoning": 0, "cache_read": 2185576, "cache_write": 380704, "total": 5141673}, "wall_s": 235.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 132.8, "cost_usd": 1.036033, "cost_reported_usd": 3.297565, "tokens": {"input": 753856, "output": 5340, "reasoning": 0, "cache_read": 542324, "cache_write": 211520, "total": 1513040}, "steps": 11, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0daa0731ffeIFU7UdpWKt15gh.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.3333333333333333, "cost_usd": 1.036033, "cost_reported_usd": 3.297565, "tokens": {"input": 753856, "output": 5340, "reasoning": 0, "cache_read": 542324, "cache_write": 211520, "total": 1513040}, "wall_s": 132.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.8888888888888888, "secondary_score": 0.6666666666666666, "returncode": 1, "wall_s": 289.7, "cost_usd": 0.852317, "cost_reported_usd": 3.738099, "tokens": {"input": 2886009, "output": 19816, "reasoning": 0, "cache_read": 2481928, "cache_write": 403854, "total": 5791607}, "steps": 42, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 26, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da9e51cffehej4cpO081bNXv.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.8888888888888888, "secondary_score": 0.6666666666666666, "cost_usd": 0.852317, "cost_reported_usd": 3.738099, "tokens": {"input": 2886009, "output": 19816, "reasoning": 0, "cache_read": 2481928, "cache_write": 403854, "total": 5791607}, "wall_s": 289.7, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: manual run delivers the feed for its trigger day (2026-03-03)", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 124.8, "cost_usd": 0.817085, "cost_reported_usd": 3.494741, "tokens": {"input": 892566, "output": 5180, "reasoning": 0, "cache_read": 755863, "cache_write": 136689, "total": 1790298}, "steps": 13, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 2, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da6e0fdffesE0U0aOrwTDAJe.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 0.6666666666666666, "cost_usd": 0.817085, "cost_reported_usd": 3.494741, "tokens": {"input": 892566, "output": 5180, "reasoning": 0, "cache_read": 755863, "cache_write": 136689, "total": 1790298}, "wall_s": 124.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 122.8, "cost_usd": 1.050531, "cost_reported_usd": 3.534747, "tokens": {"input": 828085, "output": 4897, "reasoning": 0, "cache_read": 616879, "cache_write": 211193, "total": 1661054}, "steps": 12, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da8a020ffeOvml6QjRtQL45j.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.3333333333333333, "cost_usd": 1.050531, "cost_reported_usd": 3.534747, "tokens": {"input": 828085, "output": 4897, "reasoning": 0, "cache_read": 616879, "cache_write": 211193, "total": 1661054}, "wall_s": 122.8, "airflow_skills_used": [], "skill_invocations": [{"name": "creating-dbt-models", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 64.4, "cost_usd": 0.231081, "cost_reported_usd": 0.838345, "tokens": {"input": 607309, "output": 2997, "reasoning": 0, "cache_read": 472199, "cache_write": 135065, "total": 1217570}, "steps": 9, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da47a73ffeTtWuD8QPty0UHh.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "cost_usd": 0.231081, "cost_reported_usd": 0.838345, "tokens": {"input": 607309, "output": 2997, "reasoning": 0, "cache_read": 472199, "cache_write": 135065, "total": 1217570}, "wall_s": 64.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 100.5, "cost_usd": 0.932901, "cost_reported_usd": 2.745822, "tokens": {"input": 604317, "output": 3456, "reasoning": 0, "cache_read": 401484, "cache_write": 202823, "total": 1212080}, "steps": 9, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da46326ffeywoVff4p48fZl7.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.932901, "cost_reported_usd": 2.745822, "tokens": {"input": 604317, "output": 3456, "reasoning": 0, "cache_read": 401484, "cache_write": 202823, "total": 1212080}, "wall_s": 100.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: file sensor is reschedule-mode or deferrable", "primary_pass": false, "primary_score": 0.9166666666666666, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 165.0, "cost_usd": 0.477223, "cost_reported_usd": 2.319464, "tokens": {"input": 1842387, "output": 9196, "reasoning": 0, "cache_read": 1627569, "cache_write": 214672, "total": 3693824}, "steps": 26, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 17, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da78be3ffe6FhgDBoZNprySk.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.9166666666666666, "secondary_score": 0.6666666666666666, "cost_usd": 0.477223, "cost_reported_usd": 2.319464, "tokens": {"input": 1842387, "output": 9196, "reasoning": 0, "cache_read": 1627569, "cache_write": 214672, "total": 3693824}, "wall_s": 165.0, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: DAG files import with no Variables available, DAG warehouse_load is present", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 98.6, "cost_usd": 0.966089, "cost_reported_usd": 3.174419, "tokens": {"input": 736122, "output": 3452, "reasoning": 0, "cache_read": 535113, "cache_write": 200997, "total": 1475684}, "steps": 11, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da2ffd0ffe2DkNR3Dq0y31k0.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.3333333333333333, "cost_usd": 0.966089, "cost_reported_usd": 3.174419, "tokens": {"input": 736122, "output": 3452, "reasoning": 0, "cache_read": 535113, "cache_write": 200997, "total": 1475684}, "wall_s": 98.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 70.4, "cost_usd": 0.325181, "cost_reported_usd": 1.066419, "tokens": {"input": 741295, "output": 3342, "reasoning": 0, "cache_read": 537507, "cache_write": 203731, "total": 1485875}, "steps": 11, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 3, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da25b16ffeikE50EIj6u1Ht7.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.325181, "cost_reported_usd": 1.066419, "tokens": {"input": 741295, "output": 3342, "reasoning": 0, "cache_read": 537507, "cache_write": 203731, "total": 1485875}, "wall_s": 70.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: load tasks follow the deployed catalog snapshot (config/tables.json)", "primary_pass": false, "primary_score": 0.9, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 171.0, "cost_usd": 0.737863, "cost_reported_usd": 2.445287, "tokens": {"input": 1707558, "output": 10600, "reasoning": 0, "cache_read": 1260479, "cache_write": 446945, "total": 3425582}, "steps": 24, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 16, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da3976dffeMAZG3o93jwEj6z.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.9, "secondary_score": 0.6666666666666666, "cost_usd": 0.737863, "cost_reported_usd": 2.445287, "tokens": {"input": 1707558, "output": 10600, "reasoning": 0, "cache_read": 1260479, "cache_write": 446945, "total": 3425582}, "wall_s": 171.0, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: snapshots and audit records match the Airflow 2 output", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.0, "returncode": 0, "wall_s": 126.9, "cost_usd": 1.061606, "cost_reported_usd": 3.922297, "tokens": {"input": 953579, "output": 5043, "reasoning": 0, "cache_read": 750710, "cache_write": 202854, "total": 1912186}, "steps": 14, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da0bc38ffe7QFR9gSHh8OB7L.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.0, "cost_usd": 1.061606, "cost_reported_usd": 3.922297, "tokens": {"input": 953579, "output": 5043, "reasoning": 0, "cache_read": 750710, "cache_write": 202854, "total": 1912186}, "wall_s": 126.9, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: hourly_pageviews keeps its tasks and dependencies, dags test succeeds, scheduled hourly runs succeed on 3.3, hourly rollups (previous clock hour, change vs hour before) match 2.11, completion markers match 2.11, staging cleanup keeps and deletes the same files as 2.11", "primary_pass": false, "primary_score": 0.14285714285714285, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 211.2, "cost_usd": 0.59655, "cost_reported_usd": 2.194041, "tokens": {"input": 1597614, "output": 15160, "reasoning": 0, "cache_read": 1283684, "cache_write": 313807, "total": 3210265}, "steps": 24, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 13, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da16e4cffeBC5bRnuZB0WuhX.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.14285714285714285, "secondary_score": 0.3333333333333333, "cost_usd": 0.59655, "cost_reported_usd": 2.194041, "tokens": {"input": 1597614, "output": 15160, "reasoning": 0, "cache_read": 1283684, "cache_write": 313807, "total": 3210265}, "wall_s": 211.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: hourly rollups (previous clock hour, change vs hour before) match 2.11, completion markers match 2.11, staging cleanup keeps and deletes the same files as 2.11", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 285.6, "cost_usd": 2.353638, "cost_reported_usd": 9.002781, "tokens": {"input": 2216412, "output": 11495, "reasoning": 0, "cache_read": 1776901, "cache_write": 439480, "total": 4444288}, "steps": 30, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 34, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da1c410ffeHUMn7I4LsuRanG.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "cost_usd": 2.353638, "cost_reported_usd": 9.002781, "tokens": {"input": 2216412, "output": 11495, "reasoning": 0, "cache_read": 1776901, "cache_write": 439480, "total": 4444288}, "wall_s": 285.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: snapshots and audit records match the Airflow 2 output", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.3333333333333333, "returncode": 0, "wall_s": 247.4, "cost_usd": 0.687087, "cost_reported_usd": 3.025178, "tokens": {"input": 2338282, "output": 18733, "reasoning": 0, "cache_read": 2025550, "cache_write": 312541, "total": 4695106}, "steps": 34, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 27, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0da04e4dffe1VrTNBGK7viSW5.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8, "secondary_score": 0.3333333333333333, "cost_usd": 0.687087, "cost_reported_usd": 3.025178, "tokens": {"input": 2338282, "output": 18733, "reasoning": 0, "cache_read": 2025550, "cache_write": 312541, "total": 4695106}, "wall_s": 247.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: scheduled runs process the same business day and write the same output as 2.11", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 309.7, "cost_usd": 2.239736, "cost_reported_usd": 10.315703, "tokens": {"input": 2692026, "output": 11220, "reasoning": 0, "cache_read": 2325691, "cache_write": 366298, "total": 5395235}, "steps": 36, "termination": {"why_model_stopped": "stop", "why_harness_stopped": "none", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 25, "edited_files": 5, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d9e4657fferLHFRiu0xgvfhf.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.8571428571428571, "secondary_score": 0.6666666666666666, "cost_usd": 2.239736, "cost_reported_usd": 10.315703, "tokens": {"input": 2692026, "output": 11220, "reasoning": 0, "cache_read": 2325691, "cache_write": 366298, "total": 5395235}, "wall_s": 309.7, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: DAGs import cleanly on Airflow 3.3, both DAGs keep their tasks and dependencies, carrier_scorecard is still scheduled on the asset ingest_shipments updates, dags test succeeds (ingest_shipments, carrier_scorecard)", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 219.2, "cost_usd": 0.475022, "cost_reported_usd": 2.375216, "tokens": {"input": 1900348, "output": 15869, "reasoning": 0, "cache_read": 1721495, "cache_write": 178699, "total": 3816411}, "steps": 28, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 19, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d9be553ffexmGGTfVUOfWkA0.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.475022, "cost_reported_usd": 2.375216, "tokens": {"input": 1900348, "output": 15869, "reasoning": 0, "cache_read": 1721495, "cache_write": 178699, "total": 3816411}, "wall_s": 219.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.42857142857142855, "secondary_score": 0.6666666666666666, "returncode": 1, "wall_s": 301.8, "cost_usd": 0.763037, "cost_reported_usd": 3.710157, "tokens": {"input": 2947349, "output": 17038, "reasoning": 0, "cache_read": 2614158, "cache_write": 332962, "total": 5911507}, "steps": 42, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 23, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d9d9549ffe6Tpi9KZgAf2laB.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.42857142857142855, "secondary_score": 0.6666666666666666, "cost_usd": 0.763037, "cost_reported_usd": 3.710157, "tokens": {"input": 2947349, "output": 17038, "reasoning": 0, "cache_read": 2614158, "cache_write": 332962, "total": 5911507}, "wall_s": 301.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: three hourly loads and the scorecard run succeed on 3.3, every carrier event is loaded exactly once, in the same batches as 2.11, scorecard and _READY marker match 2.11", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 355.9, "cost_usd": 2.793506, "cost_reported_usd": 10.939598, "tokens": {"input": 2715398, "output": 14354, "reasoning": 0, "cache_read": 2204209, "cache_write": 511155, "total": 5445116}, "steps": 33, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 30, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d9cba00ffeTWzQJfNp5S1e33.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.5714285714285714, "secondary_score": 0.6666666666666666, "cost_usd": 2.793506, "cost_reported_usd": 10.939598, "tokens": {"input": 2715398, "output": 14354, "reasoning": 0, "cache_read": 2204209, "cache_write": 511155, "total": 5445116}, "wall_s": 355.9, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: catchup backfills exactly 09-01..09-29, re-running a day repairs it and keeps other days", "primary_pass": false, "primary_score": 0.7777777777777778, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 108.7, "cost_usd": 0.820934, "cost_reported_usd": 3.514445, "tokens": {"input": 897850, "output": 3743, "reasoning": 0, "cache_read": 754243, "cache_write": 143594, "total": 1799430}, "steps": 12, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}, {"name": "developing-incremental-models", "origin": "global", "status": "completed"}], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 1, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d982d27ffepIM1IM4axkEH5O.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.7777777777777778, "secondary_score": 0.6666666666666666, "cost_usd": 0.820934, "cost_reported_usd": 3.514445, "tokens": {"input": 897850, "output": 3743, "reasoning": 0, "cache_read": 754243, "cache_write": 143594, "total": 1799430}, "wall_s": 108.7, "airflow_skills_used": [], "skill_invocations": [{"name": "dbt-develop", "origin": "builtin", "status": "completed"}, {"name": "developing-incremental-models", "origin": "global", "status": "completed"}], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: dags test succeeds (weekly_campaign_report, campaign_sync), scheduled Monday 06:00 runs succeed on 3.3, Monday runs report on the previous week, same output as 2.11, season backfill: scheduler creates the same weekly runs from start_date as 2.11, late-report alert still fires at 09:00 UTC for the Monday run (Deadline Alert)", "primary_pass": false, "primary_score": 0.2857142857142857, "secondary_score": 0.0, "returncode": 0, "wall_s": 215.3, "cost_usd": 0.709268, "cost_reported_usd": 2.930556, "tokens": {"input": 2221466, "output": 12617, "reasoning": 0, "cache_read": 1852700, "cache_write": 368588, "total": 4455371}, "steps": 31, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 21, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d985c2fffeWYeiSRuZQPrIP5.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.2857142857142857, "secondary_score": 0.0, "cost_usd": 0.709268, "cost_reported_usd": 2.930556, "tokens": {"input": 2221466, "output": 12617, "reasoning": 0, "cache_read": 1852700, "cache_write": 368588, "total": 4455371}, "wall_s": 215.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: one writer at a time, each run loads a day whose export has already landed", "primary_pass": false, "primary_score": 0.7777777777777778, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 70.4, "cost_usd": 0.317743, "cost_reported_usd": 0.98397, "tokens": {"input": 666279, "output": 3499, "reasoning": 0, "cache_read": 463120, "cache_write": 203107, "total": 1336005}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 4, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d962d05ffeBQuMnop3NWDUZr.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.7777777777777778, "secondary_score": 0.6666666666666666, "cost_usd": 0.317743, "cost_reported_usd": 0.98397, "tokens": {"input": 666279, "output": 3499, "reasoning": 0, "cache_read": 463120, "cache_write": 203107, "total": 1336005}, "wall_s": 70.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "returncode": 1, "wall_s": 344.0, "cost_usd": 3.218688, "cost_reported_usd": 12.417246, "tokens": {"input": 3066227, "output": 12148, "reasoning": 0, "cache_read": 2452711, "cache_write": 613475, "total": 6144561}, "steps": 40, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 34, "edited_files": 3, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d98a8c9ffeuYSzZAs3BRA8Jd.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.7142857142857143, "secondary_score": 0.6666666666666666, "cost_usd": 3.218688, "cost_reported_usd": 12.417246, "tokens": {"input": 3066227, "output": 12148, "reasoning": 0, "cache_read": 2452711, "cache_write": 613475, "total": 6144561}, "wall_s": 344.0, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 177.1, "cost_usd": 1.4594, "cost_reported_usd": 5.146007, "tokens": {"input": 1228887, "output": 5588, "reasoning": 0, "cache_read": 937024, "cache_write": 291845, "total": 2463344}, "steps": 17, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 10, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d95a36bffe013Sc6Br6DDdS7.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 1.4594, "cost_reported_usd": 5.146007, "tokens": {"input": 1228887, "output": 5588, "reasoning": 0, "cache_read": 937024, "cache_write": 291845, "total": 2463344}, "wall_s": 177.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 100.6, "cost_usd": 0.720754, "cost_reported_usd": 2.726242, "tokens": {"input": 668507, "output": 3851, "reasoning": 0, "cache_read": 534465, "cache_write": 134031, "total": 1340854}, "steps": 10, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 2, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d93f0cdffeYtdsWifEiiEBfa.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.720754, "cost_reported_usd": 2.726242, "tokens": {"input": 668507, "output": 3851, "reasoning": 0, "cache_read": 534465, "cache_write": 134031, "total": 1340854}, "wall_s": 100.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: runs at 06:00 New York on weekdays only, across DST, Monday run after DST starts reports Friday, Tuesday run reports Monday, Monday run after DST ends reports Friday, airflow dags test succeeds", "primary_pass": false, "primary_score": 0.2857142857142857, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 215.3, "cost_usd": 0.664647, "cost_reported_usd": 3.086827, "tokens": {"input": 2422364, "output": 13888, "reasoning": 0, "cache_read": 2115393, "cache_write": 306787, "total": 4858432}, "steps": 35, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 17, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d928b66ffe9q20PNgurxPHWA.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.2857142857142857, "secondary_score": 0.6666666666666666, "cost_usd": 0.664647, "cost_reported_usd": 3.086827, "tokens": {"input": 2422364, "output": 13888, "reasoning": 0, "cache_read": 2115393, "cache_write": 306787, "total": 4858432}, "wall_s": 215.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "returncode": 0, "wall_s": 338.0, "cost_usd": 0.917543, "cost_reported_usd": 2.98971, "tokens": {"input": 2072330, "output": 29095, "reasoning": 0, "cache_read": 1581134, "cache_write": 491033, "total": 4173592}, "steps": 32, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 20, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d9429c5ffeDfAuHX2DAko0SZ.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 0.6666666666666666, "cost_usd": 0.917543, "cost_reported_usd": 2.98971, "tokens": {"input": 2072330, "output": 29095, "reasoning": 0, "cache_read": 1581134, "cache_write": 491033, "total": 4173592}, "wall_s": 338.0, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 239.4, "cost_usd": 0.596205, "cost_reported_usd": 2.787421, "tokens": {"input": 2191372, "output": 18235, "reasoning": 0, "cache_read": 1942736, "cache_write": 248480, "total": 4400823}, "steps": 32, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "idle-done", "done_reason": "idle_heuristic"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 8, "edited_files": 6, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d91da16ffer2wCI3yvA040Sg.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.596205, "cost_reported_usd": 2.787421, "tokens": {"input": 2191372, "output": 18235, "reasoning": 0, "cache_read": 1942736, "cache_write": 248480, "total": 4400823}, "wall_s": 239.4, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 271.6, "cost_usd": 1.457946, "cost_reported_usd": 6.604182, "tokens": {"input": 1715437, "output": 13593, "reasoning": 0, "cache_read": 1501107, "cache_write": 214305, "total": 3444442}, "steps": 24, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 14, "edited_files": 5, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d924f8dffegxl2F0yo2vtNXg.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 1.457946, "cost_reported_usd": 6.604182, "tokens": {"input": 1715437, "output": 13593, "reasoning": 0, "cache_read": 1501107, "cache_write": 214305, "total": 3444442}, "wall_s": 271.6, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 132.8, "cost_usd": 0.79112, "cost_reported_usd": 3.140303, "tokens": {"input": 783073, "output": 4831, "reasoning": 0, "cache_read": 642858, "cache_write": 140203, "total": 1570965}, "steps": 11, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 9, "edited_files": 1, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8de009ffe5ddSJCYB550PCN.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.79112, "cost_reported_usd": 3.140303, "tokens": {"input": 783073, "output": 4831, "reasoning": 0, "cache_read": 642858, "cache_write": 140203, "total": 1570965}, "wall_s": 132.8, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 231.5, "cost_usd": 1.575409, "cost_reported_usd": 6.03793, "tokens": {"input": 1487529, "output": 10496, "reasoning": 0, "cache_read": 1205869, "cache_write": 281638, "total": 2985532}, "steps": 21, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 18, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8ebabbffeiTCaU1mZ2802tn.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 1.575409, "cost_reported_usd": 6.03793, "tokens": {"input": 1487529, "output": 10496, "reasoning": 0, "cache_read": 1205869, "cache_write": 281638, "total": 2985532}, "wall_s": 231.5, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "ok", "reason": "primary checks passed", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "returncode": 0, "wall_s": 203.1, "cost_usd": 0.528536, "cost_reported_usd": 2.601319, "tokens": {"input": 2072939, "output": 9351, "reasoning": 0, "cache_read": 1834220, "cache_write": 238562, "total": 4155072}, "steps": 31, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "idle-done", "done_reason": "idle_heuristic"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 11, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8ddf64ffeT1CLE3mqWs6wns.json", "attempt": 0}], "status": "ok", "primary_pass": true, "primary_score": 1.0, "secondary_score": 1.0, "cost_usd": 0.528536, "cost_reported_usd": 2.601319, "tokens": {"input": 2072939, "output": 9351, "reasoning": 0, "cache_read": 1834220, "cache_write": 238562, "total": 4155072}, "wall_s": 203.1, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-airflow2-inventory-snapshot", "area": "authoring", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-clickstream-claim-check", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-daily-revenue-yesterday", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-partner-feeds-mapping", "area": "authoring", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "authoring-vendor-shipments-etl", "area": "authoring", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-manual-trigger-context", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-nondeterministic-parse", "area": "debugging", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "debugging-variable-at-parse", "area": "debugging", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-hourly-pageviews-templates", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-inventory-context", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-revenue-previous-day", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-shipments-assets", "area": "migration", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "migration-weekly-catchup", "area": "migration", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-backfill-pageviews", "area": "scheduling", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-idempotent-orders-load", "area": "scheduling", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "scheduling-weekday-ny-business-day", "area": "scheduling", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-airflow2-ci-suite", "area": "testing", "split": "dev", "airflow_version": "2.11", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-dagbag-integrity", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 3, "attempts": [{"status": "skipped_budget", "reason": "campaign budget exhausted"}], "status": "skipped_budget", "primary_pass": false, "primary_score": null, "secondary_score": null, "cost_usd": 0, "cost_reported_usd": 0, "tokens": null, "wall_s": null, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-custom-operator-conn", "area": "testing", "split": "dev", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "turn_limit", "reason": "max turns reached", "primary_pass": false, "primary_score": 0.125, "secondary_score": 0.75, "returncode": 1, "wall_s": 305.7, "cost_usd": 0.99448, "cost_reported_usd": 3.911208, "tokens": {"input": 2916948, "output": 21252, "reasoning": 0, "cache_read": 2398183, "cache_write": 518545, "total": 5854928}, "steps": 43, "termination": {"why_model_stopped": "tool-call", "why_harness_stopped": "budget-exhausted", "done_reason": "none"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 17, "edited_files": 4, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8e6621ffeqsXdpuF4ESKi8F.json", "attempt": 0}], "status": "turn_limit", "primary_pass": false, "primary_score": 0.125, "secondary_score": 0.75, "cost_usd": 0.99448, "cost_reported_usd": 3.911208, "tokens": {"input": 2916948, "output": 21252, "reasoning": 0, "cache_read": 2398183, "cache_write": 518545, "total": 5854928}, "wall_s": 305.7, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-sonnet-4-6@default", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: suite catches hidden bug: validation_bypassed", "primary_pass": false, "primary_score": 0.875, "secondary_score": 1.0, "returncode": 0, "wall_s": 249.3, "cost_usd": 1.590801, "cost_reported_usd": 5.95212, "tokens": {"input": 1453794, "output": 12004, "reasoning": 0, "cache_read": 1171296, "cache_write": 282477, "total": 2919571}, "steps": 20, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "none", "done_reason": "explicit_done"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 13, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8b8fa8ffe0RFtWJMZJw4Z2F.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.875, "secondary_score": 1.0, "cost_usd": 1.590801, "cost_reported_usd": 5.95212, "tokens": {"input": 1453794, "output": 12004, "reasoning": 0, "cache_read": 1171296, "cache_write": 282477, "total": 2919571}, "wall_s": 249.3, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} +{"case": "testing-taskflow-transforms", "area": "testing", "split": "holdout", "airflow_version": "3.3", "arm": "baseline", "model": "google-vertex-anthropic/claude-haiku-4-5@20251001", "run": 2, "attempts": [{"status": "task_fail", "reason": "failed primary: suite catches hidden bug: validation_bypassed, suite catches hidden bug: schedule_midnight", "primary_pass": false, "primary_score": 0.75, "secondary_score": 1.0, "returncode": 0, "wall_s": 255.2, "cost_usd": 0.526737, "cost_reported_usd": 1.960422, "tokens": {"input": 1433791, "output": 24934, "reasoning": 0, "cache_read": 1208822, "cache_write": 224863, "total": 2892410}, "steps": 23, "termination": {"why_model_stopped": "explicit-done", "why_harness_stopped": "idle-done", "done_reason": "idle_heuristic"}, "skill_invocations": [], "auto_loaded_skills": [], "airflow_skills_used": [], "bash_commands": 7, "edited_files": 2, "errors": [], "trace_path": "~/.local/share/altimate-code/traces/ses_f0d8acd6effeuNc2hWN0Nsq8es.json", "attempt": 0}], "status": "task_fail", "primary_pass": false, "primary_score": 0.75, "secondary_score": 1.0, "cost_usd": 0.526737, "cost_reported_usd": 1.960422, "tokens": {"input": 1433791, "output": 24934, "reasoning": 0, "cache_read": 1208822, "cache_write": 224863, "total": 2892410}, "wall_s": 255.2, "airflow_skills_used": [], "skill_invocations": [], "retries": 0} diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..53cdb29 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,106 @@ +diff --git a/dags/inventory_snapshot.py b/dags/inventory_snapshot.py +new file mode 100644 +index 0000000..90f9b3c +--- /dev/null ++++ b/dags/inventory_snapshot.py +@@ -0,0 +1,100 @@ ++"""Daily inventory snapshot with low stock alerts. ++ ++Computes end-of-day stock on hand per warehouse and SKU, then identifies ++items below their reorder point. ++ ++Convention for this repo: every daily DAG writes one file per logical date ++(`{{ ds }}`) under output//, so a rerun simply overwrites that day. ++""" ++ ++from __future__ import annotations ++ ++import csv ++from collections import defaultdict ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.decorators import dag, task ++from airflow.operators.empty import EmptyOperator ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DATA_DIR = PROJECT_ROOT / "data" ++OUTPUT_DIR = PROJECT_ROOT / "output" ++ ++ ++@dag( ++ dag_id="inventory_snapshot", ++ schedule="30 2 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, ++ tags=["inventory"], ++) ++def inventory_snapshot(): ++ start = EmptyOperator(task_id="start") ++ ++ @task ++ def compute_snapshot(ds=None) -> str: ++ """Compute end-of-day on-hand stock per warehouse and SKU as of ds.""" ++ # Load all inventory movements up to and including the run date ++ on_hand: dict[tuple[str, str], float] = defaultdict(float) ++ with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ if row["movement_date"] <= ds: ++ key = (row["warehouse_id"], row["sku"]) ++ on_hand[key] += float(row["qty_change"]) ++ ++ # Write snapshot ++ out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand"]) ++ # Sort by warehouse_id then sku ++ for (warehouse_id, sku) in sorted(on_hand.keys()): ++ writer.writerow([warehouse_id, sku, on_hand[(warehouse_id, sku)]]) ++ ++ return str(out) ++ ++ @task ++ def identify_low_stock(ds=None) -> str: ++ """Identify items with on-hand below reorder point.""" ++ # Load reorder points ++ reorder_points: dict[str, float] = {} ++ with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ reorder_points[row["sku"]] = float(row["reorder_point"]) ++ ++ # Load inventory snapshot for the run date ++ snapshot: dict[tuple[str, str], float] = {} ++ snapshot_file = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ with snapshot_file.open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ key = (row["warehouse_id"], row["sku"]) ++ snapshot[key] = float(row["on_hand"]) ++ ++ # Find low stock items ++ low_stock = [] ++ for (warehouse_id, sku), on_hand in snapshot.items(): ++ reorder_point = reorder_points.get(sku, 0) ++ if on_hand < reorder_point: ++ low_stock.append((warehouse_id, sku, on_hand, reorder_point)) ++ ++ # Write low stock file, sorted by warehouse_id then sku ++ out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand", "reorder_point"]) ++ for warehouse_id, sku, on_hand, reorder_point in sorted(low_stock): ++ writer.writerow([warehouse_id, sku, on_hand, reorder_point]) ++ ++ return str(out) ++ ++ snapshot_task = compute_snapshot() ++ low_stock_task = identify_low_stock() ++ start >> snapshot_task >> low_stock_task ++ ++ ++inventory_snapshot() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..b20e225 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 100.5, + "cost_usd": 0.284432, + "cost_reported_usd": 1.23052, + "tokens": { + "input": 946165, + "output": 6420, + "reasoning": 0, + "cache_read": 809004, + "cache_write": 137084, + "total": 1898673 + }, + "steps": 14, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 10, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd66033ffeCN3SvKAbT0C4br.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..f7f7b28 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly in Airflow 2.11", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG inventory_snapshot exists", + "kind": "primary", + "passed": true, + "detail": "found: ['inventory_snapshot', 'sales_daily']" + }, + { + "name": "scheduled daily at 02:30 UTC", + "kind": "primary", + "passed": true, + "detail": "[{'start': '2026-10-01T02:30:00+00:00', 'end': '2026-10-02T02:30:00+00:00', 'run_after': '2026-10-02T02:30:00+00:00'}, {'start': '2026-10-02T02:30:00+00:00', 'end': '2026-10-03T02:30:00+00:00', 'run_after': '2026-10-03T02:30:00+00:00'}, {'start': '2026-10-03T02:30:00+00:00', 'end': '2026-10-04T02:30:00+00:00', 'run_after': '2026-10-04T02:30:00+00:00'}]" + }, + { + "name": "catchup disabled (2.11 defaults to catchup=True)", + "kind": "primary", + "passed": true, + "detail": "catchup=False" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "snapshot and low-stock files are correct for each logical date", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun is idempotent and keeps other dates", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; " + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "10 bash commands, none verifying" + }, + { + "name": "ruff AIR0,AIR2 clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..5e18965 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch @@ -0,0 +1,106 @@ +diff --git a/dags/inventory_snapshot.py b/dags/inventory_snapshot.py +new file mode 100644 +index 0000000..1e41a4d +--- /dev/null ++++ b/dags/inventory_snapshot.py +@@ -0,0 +1,100 @@ ++"""Daily inventory snapshot with low stock alerts. ++ ++Convention for this repo: every daily DAG writes one file per logical date ++(`{{ ds }}`) under output//, so a rerun simply overwrites that day. ++""" ++ ++from __future__ import annotations ++ ++import csv ++from collections import defaultdict ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.decorators import dag, task ++from airflow.operators.empty import EmptyOperator ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DATA_DIR = PROJECT_ROOT / "data" ++OUTPUT_DIR = PROJECT_ROOT / "output" ++ ++ ++@dag( ++ dag_id="inventory_snapshot", ++ schedule="30 2 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, ++ tags=["inventory"], ++) ++def inventory_snapshot(): ++ start = EmptyOperator(task_id="start") ++ ++ @task ++ def calculate_snapshot(ds=None) -> str: ++ """Calculate end-of-day stock on hand per warehouse and SKU.""" ++ on_hand: dict[tuple[str, str], float] = defaultdict(float) ++ ++ with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ if row["movement_date"] <= ds: ++ key = (row["warehouse_id"], row["sku"]) ++ on_hand[key] += float(row["qty_change"]) ++ ++ out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ ++ # Sort by warehouse_id, then sku ++ sorted_keys = sorted(on_hand.keys()) ++ ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand"]) ++ for warehouse_id, sku in sorted_keys: ++ writer.writerow([warehouse_id, sku, int(on_hand[(warehouse_id, sku)])]) ++ ++ return str(out) ++ ++ @task ++ def identify_low_stock(ds=None) -> str: ++ """Identify items below reorder point.""" ++ # Load reorder points ++ reorder_points: dict[str, float] = {} ++ with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ reorder_points[row["sku"]] = float(row["reorder_point"]) ++ ++ # Load current inventory snapshot ++ on_hand: dict[tuple[str, str], float] = {} ++ snapshot_file = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ with snapshot_file.open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ key = (row["warehouse_id"], row["sku"]) ++ on_hand[key] = float(row["on_hand"]) ++ ++ # Find low stock items ++ low_stock = [] ++ for (warehouse_id, sku), qty in on_hand.items(): ++ reorder_point = reorder_points.get(sku, 0) ++ if qty < reorder_point: ++ low_stock.append((warehouse_id, sku, qty, reorder_point)) ++ ++ # Sort by warehouse_id, then sku ++ low_stock.sort(key=lambda x: (x[0], x[1])) ++ ++ out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand", "reorder_point"]) ++ for warehouse_id, sku, qty, reorder_point in low_stock: ++ writer.writerow([warehouse_id, sku, int(qty), int(reorder_point)]) ++ ++ return str(out) ++ ++ start >> calculate_snapshot() >> identify_low_stock() ++ ++ ++inventory_snapshot() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..d730363 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 108.5, + "cost_usd": 0.461784, + "cost_reported_usd": 1.624657, + "tokens": { + "input": 1162971, + "output": 6175, + "reasoning": 0, + "cache_read": 889374, + "cache_write": 273499, + "total": 2332019 + }, + "steps": 17, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 13, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dada241ffe7NBnmWGHbI3lJ1.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json new file mode 100644 index 0000000..0615445 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly in Airflow 2.11", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG inventory_snapshot exists", + "kind": "primary", + "passed": true, + "detail": "found: ['inventory_snapshot', 'sales_daily']" + }, + { + "name": "scheduled daily at 02:30 UTC", + "kind": "primary", + "passed": true, + "detail": "[{'start': '2026-10-01T02:30:00+00:00', 'end': '2026-10-02T02:30:00+00:00', 'run_after': '2026-10-02T02:30:00+00:00'}, {'start': '2026-10-02T02:30:00+00:00', 'end': '2026-10-03T02:30:00+00:00', 'run_after': '2026-10-03T02:30:00+00:00'}, {'start': '2026-10-03T02:30:00+00:00', 'end': '2026-10-04T02:30:00+00:00', 'run_after': '2026-10-04T02:30:00+00:00'}]" + }, + { + "name": "catchup disabled (2.11 defaults to catchup=True)", + "kind": "primary", + "passed": true, + "detail": "catchup=False" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "snapshot and low-stock files are correct for each logical date", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun is idempotent and keeps other dates", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; " + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "13 bash commands, none verifying" + }, + { + "name": "ruff AIR0,AIR2 clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..50d5e3b --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,92 @@ +diff --git a/dags/inventory_snapshot.py b/dags/inventory_snapshot.py +new file mode 100644 +index 0000000..d9c505b +--- /dev/null ++++ b/dags/inventory_snapshot.py +@@ -0,0 +1,86 @@ ++"""Daily inventory snapshot per warehouse and SKU. ++ ++Convention for this repo: every daily DAG writes one file per logical date ++(`{{ ds }}`) under output//, so a rerun simply overwrites that day. ++ ++Tasks ++----- ++1. snapshot – end-of-day stock on hand (cumulative qty_change up to and ++ including ds) written to output/inventory_snapshot/.csv ++2. low_stock – rows where on_hand < reorder_point, written to ++ output/low_stock/.csv ++""" ++ ++from __future__ import annotations ++ ++import csv ++from collections import defaultdict ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.decorators import dag, task ++from airflow.operators.empty import EmptyOperator ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DATA_DIR = PROJECT_ROOT / "data" ++OUTPUT_DIR = PROJECT_ROOT / "output" ++ ++ ++@dag( ++ dag_id="inventory_snapshot", ++ schedule="30 2 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, ++ tags=["inventory"], ++) ++def inventory_snapshot(): ++ start = EmptyOperator(task_id="start") ++ ++ @task ++ def snapshot(ds=None) -> str: ++ """Compute cumulative on-hand stock up to and including ds.""" ++ on_hand: dict[tuple[str, str], int] = defaultdict(int) ++ with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ if row["movement_date"] <= ds: ++ key = (row["warehouse_id"], row["sku"]) ++ on_hand[key] += int(row["qty_change"]) ++ ++ out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand"]) ++ for (warehouse_id, sku) in sorted(on_hand): ++ writer.writerow([warehouse_id, sku, on_hand[(warehouse_id, sku)]]) ++ return str(out) ++ ++ @task ++ def low_stock(snapshot_path: str, ds=None) -> str: ++ """Write rows where on_hand is below the SKU's reorder point.""" ++ reorder: dict[str, int] = {} ++ with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ reorder[row["sku"]] = int(row["reorder_point"]) ++ ++ out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand", "reorder_point"]) ++ with open(snapshot_path, newline="") as snap_fh: ++ for row in csv.DictReader(snap_fh): ++ sku = row["sku"] ++ on_hand_val = int(row["on_hand"]) ++ rp = reorder.get(sku) ++ if rp is not None and on_hand_val < rp: ++ writer.writerow([row["warehouse_id"], sku, on_hand_val, rp]) ++ return str(out) ++ ++ snapshot_task = snapshot() ++ start >> snapshot_task >> low_stock(snapshot_task) ++ ++ ++inventory_snapshot() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..33c4cf0 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 90.5, + "cost_usd": 0.719271, + "cost_reported_usd": 2.477547, + "tokens": { + "input": 586101, + "output": 3114, + "reasoning": 0, + "cache_read": 442119, + "cache_write": 143973, + "total": 1175307 + }, + "steps": 8, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 2, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65c1fffeLY05DTRQcSxPLb.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..6a04e9f --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly in Airflow 2.11", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG inventory_snapshot exists", + "kind": "primary", + "passed": true, + "detail": "found: ['inventory_snapshot', 'sales_daily']" + }, + { + "name": "scheduled daily at 02:30 UTC", + "kind": "primary", + "passed": true, + "detail": "[{'start': '2026-10-01T02:30:00+00:00', 'end': '2026-10-02T02:30:00+00:00', 'run_after': '2026-10-02T02:30:00+00:00'}, {'start': '2026-10-02T02:30:00+00:00', 'end': '2026-10-03T02:30:00+00:00', 'run_after': '2026-10-03T02:30:00+00:00'}, {'start': '2026-10-03T02:30:00+00:00', 'end': '2026-10-04T02:30:00+00:00', 'run_after': '2026-10-04T02:30:00+00:00'}]" + }, + { + "name": "catchup disabled (2.11 defaults to catchup=True)", + "kind": "primary", + "passed": true, + "detail": "catchup=False" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "snapshot and low-stock files are correct for each logical date", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun is idempotent and keeps other dates", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; " + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "2 bash commands, none verifying" + }, + { + "name": "ruff AIR0,AIR2 clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..77022ad --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch @@ -0,0 +1,101 @@ +diff --git a/dags/inventory_snapshot.py b/dags/inventory_snapshot.py +new file mode 100644 +index 0000000..8a64e79 +--- /dev/null ++++ b/dags/inventory_snapshot.py +@@ -0,0 +1,95 @@ ++"""Daily inventory snapshot per warehouse and SKU. ++ ++Convention for this repo: every daily DAG writes one file per logical date ++(`{{ ds }}`) under output//, so a rerun simply overwrites that day. ++ ++Tasks ++----- ++1. snapshot – end-of-day stock on hand (sum of all qty_change up to and ++ including the run date) written to ++ output/inventory_snapshot/.csv (warehouse_id,sku,on_hand) ++2. low_stock – rows where on_hand < reorder_point, written to ++ output/low_stock/.csv ++ (warehouse_id,sku,on_hand,reorder_point) ++ ++Both files are sorted by warehouse_id then sku. ++""" ++ ++from __future__ import annotations ++ ++import csv ++from collections import defaultdict ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.decorators import dag, task ++from airflow.operators.empty import EmptyOperator ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DATA_DIR = PROJECT_ROOT / "data" ++OUTPUT_DIR = PROJECT_ROOT / "output" ++ ++ ++@dag( ++ dag_id="inventory_snapshot", ++ schedule="30 2 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "supply-chain-analytics", "retries": 1, "retry_delay": timedelta(minutes=10)}, ++ tags=["inventory"], ++) ++def inventory_snapshot(): ++ start = EmptyOperator(task_id="start") ++ ++ @task ++ def snapshot(ds=None) -> str: ++ """Compute end-of-day on-hand stock for every warehouse/SKU as of ds.""" ++ on_hand: dict[tuple[str, str], int] = defaultdict(int) ++ with (DATA_DIR / "inventory_movements.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ if row["movement_date"] <= ds: ++ key = (row["warehouse_id"], row["sku"]) ++ on_hand[key] += int(row["qty_change"]) ++ ++ out = OUTPUT_DIR / "inventory_snapshot" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand"]) ++ for (warehouse_id, sku) in sorted(on_hand): ++ writer.writerow([warehouse_id, sku, on_hand[(warehouse_id, sku)]]) ++ return str(out) ++ ++ @task ++ def low_stock(snapshot_path: str, ds=None) -> str: ++ """Write rows where on_hand is below the SKU's reorder point.""" ++ reorder: dict[str, int] = {} ++ with (DATA_DIR / "reorder_points.csv").open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ reorder[row["sku"]] = int(row["reorder_point"]) ++ ++ out = OUTPUT_DIR / "low_stock" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ rows = [] ++ with open(snapshot_path, newline="") as fh: ++ for row in csv.DictReader(fh): ++ sku = row["sku"] ++ oh = int(row["on_hand"]) ++ rp = reorder.get(sku) ++ if rp is not None and oh < rp: ++ rows.append((row["warehouse_id"], sku, oh, rp)) ++ ++ rows.sort(key=lambda r: (r[0], r[1])) ++ with out.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["warehouse_id", "sku", "on_hand", "reorder_point"]) ++ for r in rows: ++ writer.writerow(r) ++ return str(out) ++ ++ snapshot_path = snapshot() ++ start >> snapshot_path >> low_stock(snapshot_path) ++ ++ ++inventory_snapshot() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..c6d581f --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 104.5, + "cost_usd": 0.74868, + "cost_reported_usd": 2.737893, + "tokens": { + "input": 663081, + "output": 3596, + "reasoning": 0, + "cache_read": 519364, + "cache_write": 143707, + "total": 1329748 + }, + "steps": 9, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 4, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0daf8722ffeW70425b2VMmwmc.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json new file mode 100644 index 0000000..9637c38 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-airflow2-inventory-snapshot/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly in Airflow 2.11", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG inventory_snapshot exists", + "kind": "primary", + "passed": true, + "detail": "found: ['inventory_snapshot', 'sales_daily']" + }, + { + "name": "scheduled daily at 02:30 UTC", + "kind": "primary", + "passed": true, + "detail": "[{'start': '2026-10-01T02:30:00+00:00', 'end': '2026-10-02T02:30:00+00:00', 'run_after': '2026-10-02T02:30:00+00:00'}, {'start': '2026-10-02T02:30:00+00:00', 'end': '2026-10-03T02:30:00+00:00', 'run_after': '2026-10-03T02:30:00+00:00'}, {'start': '2026-10-03T02:30:00+00:00', 'end': '2026-10-04T02:30:00+00:00', 'run_after': '2026-10-04T02:30:00+00:00'}]" + }, + { + "name": "catchup disabled (2.11 defaults to catchup=True)", + "kind": "primary", + "passed": true, + "detail": "catchup=False" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "snapshot and low-stock files are correct for each logical date", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun is idempotent and keeps other dates", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; " + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "4 bash commands, none verifying" + }, + { + "name": "ruff AIR0,AIR2 clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..c295aab --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,98 @@ +diff --git a/dags/clickstream_page_stats.py b/dags/clickstream_page_stats.py +new file mode 100644 +index 0000000..b593e43 +--- /dev/null ++++ b/dags/clickstream_page_stats.py +@@ -0,0 +1,92 @@ ++"""Daily page-level stats from clickstream exports.""" ++ ++from __future__ import annotations ++ ++import csv ++import gzip ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="30 5 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["web-analytics"], ++) ++def clickstream_page_stats(): ++ @task ++ def extract(ds=None) -> dict[str, list[dict]]: ++ """Extract day's clickstream data, filter bots and anonymous hits.""" ++ data_file = PROJECT_ROOT / "data" / "clickstream" / f"{ds}.csv.gz" ++ ++ # Dictionary to collect rows by page for efficient in-memory processing ++ pages = {} ++ ++ if not data_file.exists(): ++ return pages ++ ++ with gzip.open(data_file, "rt", newline="") as fh: ++ reader = csv.DictReader(fh) ++ for row in reader: ++ # Skip bots (is_bot = 1) and anonymous hits (empty user_id) ++ if row["is_bot"] == "1" or not row["user_id"]: ++ continue ++ ++ page = row["page"] ++ if page not in pages: ++ pages[page] = [] ++ pages[page].append(row) ++ ++ return pages ++ ++ @task ++ def transform(extracted_data: dict[str, list[dict]]) -> list[dict]: ++ """Compute per-page stats: views, unique_users, avg_load_ms.""" ++ stats = [] ++ ++ for page in sorted(extracted_data.keys()): ++ rows = extracted_data[page] ++ ++ # views = total events for this page ++ views = len(rows) ++ ++ # unique_users = count of distinct user_ids ++ unique_users = len(set(row["user_id"] for row in rows)) ++ ++ # avg_load_ms = mean of load_ms, rounded to 1 decimal ++ load_times = [float(row["load_ms"]) for row in rows] ++ avg_load_ms = round(sum(load_times) / len(load_times), 1) if load_times else 0.0 ++ ++ stats.append({ ++ "page": page, ++ "views": views, ++ "unique_users": unique_users, ++ "avg_load_ms": avg_load_ms, ++ }) ++ ++ return stats ++ ++ @task ++ def load(stats: list[dict], ds=None) -> None: ++ """Write output file with sorted page stats.""" ++ out = PROJECT_ROOT / "output" / "page_stats" / f"{ds}.csv" ++ out.parent.mkdir(parents=True, exist_ok=True) ++ ++ with out.open("w", newline="") as fh: ++ writer = csv.DictWriter(fh, fieldnames=["page", "views", "unique_users", "avg_load_ms"]) ++ writer.writeheader() ++ # Stats are already sorted by page from transform ++ for row in stats: ++ writer.writerow(row) ++ ++ extracted = extract() ++ transformed = transform(extracted) ++ load(transformed) ++ ++ ++clickstream_page_stats() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..02efbf6 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "task_fail", + "reason": "failed primary: no XCom/Variable value larger than 48 KB (data passed by reference)", + "primary_pass": false, + "primary_score": 0.8571428571428571, + "secondary_score": 1.0, + "returncode": 0, + "wall_s": 229.4, + "cost_usd": 0.57126, + "cost_reported_usd": 2.240903, + "tokens": { + "input": 1669775, + "output": 7936, + "reasoning": 0, + "cache_read": 1352701, + "cache_write": 316942, + "total": 3347354 + }, + "steps": 25, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 17, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65c1bffeZXPZNvOGJu2TNM.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..4eea9fc --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG clickstream_page_stats exists", + "kind": "primary", + "passed": true, + "detail": "found: ['clickstream_page_stats', 'marketing_spend_daily']" + }, + { + "name": "extract, transform and load are separate chained tasks", + "kind": "primary", + "passed": true, + "detail": "longest dependency chain: 3 tasks; deps=[['extract', 'transform'], ['transform', 'load']]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "page stats are correct for each day", + "kind": "primary", + "passed": true, + "detail": "2026-09-28: expected 32 pages, got 32 (first mismatch: none); 2026-09-29: expected 32 pages, got 32 (first mismatch: none)" + }, + { + "name": "no XCom/Variable value larger than 48 KB (data passed by reference)", + "kind": "primary", + "passed": false, + "detail": "68 XCom/Variable rows; largest: task=extract key=return_value bytes=3177484" + }, + { + "name": "rerun reproduces the output", + "kind": "primary", + "passed": true, + "detail": "rerun ok=True" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.8571428571428571, + "secondary_score": 1.0 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..e69de29 diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..0b58907 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,28 @@ +{ + "status": "infra_error", + "reason": "no model steps completed: \u001b[91m\u001b[1mError: \u001b[0mModel not found: google-vertex-anthropic/claude-haiku-4-5@20251001\nTry: `altimate models` to list available models\nOr check your config (altimate-code.json) provider/model names", + "primary_pass": false, + "primary_score": null, + "secondary_score": null, + "returncode": 1, + "wall_s": 8.0, + "cost_usd": 0.0, + "cost_reported_usd": 0.0, + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0, + "total": 0 + }, + "steps": 0, + "termination": {}, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 0, + "edited_files": 0, + "errors": [], + "trace_path": null +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/agent.patch new file mode 100644 index 0000000..a210d26 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/agent.patch @@ -0,0 +1,285 @@ +diff --git a/CLICKSTREAM_DAG.md b/CLICKSTREAM_DAG.md +new file mode 100644 +index 0000000..8a50e8e +--- /dev/null ++++ b/CLICKSTREAM_DAG.md +@@ -0,0 +1,110 @@ ++# Clickstream Page Stats DAG ++ ++Daily page-level statistics aggregation from web clickstream exports. ++ ++## Overview ++ ++**DAG ID**: `clickstream_page_stats` ++**Schedule**: Daily at 05:30 UTC ++**Start Date**: 2026-09-01 ++**Backfill**: Enabled (supports rerun from start date onwards) ++ ++## Pipeline ++ ++``` ++extract → transform → load ++``` ++ ++### 1. Extract Task ++- **Input**: `data/clickstream/{date}.csv.gz` (gzipped daily exports) ++- **Processing**: ++ - Reads raw clickstream events ++ - Filters out bot traffic (`is_bot = 1`) ++ - Filters out anonymous hits (empty `user_id`) ++- **Output**: Cleaned events (temporary parquet) ++ ++### 2. Transform Task ++- **Input**: Cleaned events from extract task ++- **Processing**: ++ - Groups events by page ++ - Computes: ++ - `views`: Total page views (COUNT(*)) ++ - `unique_users`: Distinct users (COUNT(DISTINCT user_id)) ++ - `avg_load_ms`: Mean load time rounded to 1 decimal (ROUND(AVG(load_ms), 1)) ++- **Output**: Aggregated statistics (temporary parquet) ++ ++### 3. Load Task ++- **Input**: Aggregated statistics from transform task ++- **Processing**: ++ - Sorts by page name ++ - Writes to CSV format ++- **Output**: `output/page_stats/{date}.csv` ++ ++## Output Format ++ ++CSV with columns: ++``` ++page,views,unique_users,avg_load_ms ++/,611,562,1779.4 ++/account,618,576,1768.9 ++/blog,648,599,1718.1 ++... ++``` ++ ++Sorted alphabetically by page. ++ ++## Data Quality ++ ++- **Input validation**: Checks for presence of daily export file ++- **Filtering**: Removes ~5-7% of traffic (bots and anonymous) ++- **Constraints**: Enforced unique_users ≤ views ++- **Type safety**: All numeric columns validated ++ ++## Scaling Characteristics ++ ++- **Current data**: ~22,000 events/day ++- **Projected growth**: 50x → ~1.1M events/day ++- **Architecture**: Uses DuckDB for efficient in-memory processing ++ - Memory-efficient for large datasets ++ - Fast columnar operations (GROUP BY, COUNT DISTINCT) ++ - Direct gzip and parquet I/O ++ - No external dependencies (vs. Spark) ++ ++## Rerun / Backfill ++ ++The DAG supports reruns of any day since 2026-09-01: ++ ++```bash ++# Backfill a specific date ++airflow dags backfill clickstream_page_stats --start-date 2026-09-15 --end-date 2026-09-15 ++ ++# Backfill a date range ++airflow dags backfill clickstream_page_stats --start-date 2026-09-15 --end-date 2026-09-20 ++``` ++ ++Reruns replace the output file for that day (idempotent). ++ ++## Dependencies ++ ++- `duckdb` (v1.5.6+) for data processing ++- `apache-airflow` (v3.3.2+) for orchestration ++- `pendulum` for datetime handling ++ ++All dependencies are already in `requirements.txt`. ++ ++## Monitoring ++ ++- Check DAG health in Airflow UI ++- Output files in `output/page_stats/` should appear daily ++- Each day's file contains 20-35 rows (unique pages) ++- If file count/size drops significantly, investigate upstream data ++ ++## Troubleshooting ++ ++**Missing daily export**: DAG will fail with `FileNotFoundError` if the day's export is not in `data/clickstream/` ++ ++**Zero-count metrics**: If unique_users or avg_load_ms are 0 for a page, it means: ++- No valid (non-bot, non-anonymous) events for that page ++- Check if entire day's data has exceptional filtering rate ++ ++**CSV write errors**: Ensure `output/` directory exists and is writable +diff --git a/dags/clickstream_page_stats.py b/dags/clickstream_page_stats.py +new file mode 100644 +index 0000000..58f3c4a +--- /dev/null ++++ b/dags/clickstream_page_stats.py +@@ -0,0 +1,163 @@ ++"""Daily page-level statistics from clickstream exports.""" ++ ++from __future__ import annotations ++ ++import csv ++import gzip ++from pathlib import Path ++ ++import duckdb ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="30 5 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["analytics", "clickstream"], ++) ++def clickstream_page_stats(): ++ """Extract, transform, and load daily page-level clickstream statistics. ++ ++ This DAG reads daily clickstream exports, filters out bot traffic and ++ anonymous users, computes per-page statistics (views, unique users, ++ average load time), and outputs a sorted CSV file. ++ """ ++ ++ @task ++ def extract(ds=None) -> str: ++ """Read that day's export and filter out bot traffic and anonymous hits. ++ ++ Args: ++ ds: Airflow execution date (YYYY-MM-DD format) ++ ++ Returns: ++ Path to the cleaned data parquet file ++ """ ++ input_file = PROJECT_ROOT / "data" / "clickstream" / f"{ds}.csv.gz" ++ ++ if not input_file.exists(): ++ raise FileNotFoundError(f"Clickstream export not found: {input_file}") ++ ++ # Use DuckDB for efficient filtering at scale ++ conn = duckdb.connect(":memory:") ++ ++ # Read the gzipped CSV directly ++ conn.execute(f""" ++ CREATE TABLE raw_events AS ++ SELECT * ++ FROM read_csv_auto('{input_file}', compression='gzip') ++ """) ++ ++ # Filter: exclude bot traffic (is_bot = 1) and anonymous hits (empty user_id) ++ conn.execute(""" ++ CREATE TABLE cleaned_events AS ++ SELECT * ++ FROM raw_events ++ WHERE is_bot = 0 ++ AND user_id IS NOT NULL ++ AND user_id != '' ++ """) ++ ++ # Store result in temporary parquet for next task ++ temp_file = PROJECT_ROOT / ".tmp" / f"{ds}_cleaned.parquet" ++ temp_file.parent.mkdir(parents=True, exist_ok=True) ++ ++ conn.execute(f"COPY cleaned_events TO '{temp_file}' (FORMAT PARQUET)") ++ conn.close() ++ ++ return str(temp_file) ++ ++ @task ++ def transform(cleaned_data_path: str, ds=None) -> str: ++ """Compute per-page statistics: views, unique_users, avg_load_ms. ++ ++ Args: ++ cleaned_data_path: Path to cleaned events parquet file ++ ds: Airflow execution date (YYYY-MM-DD format) ++ ++ Returns: ++ Path to the aggregated statistics parquet file ++ """ ++ conn = duckdb.connect(":memory:") ++ ++ # Load cleaned events ++ conn.execute(f""" ++ CREATE TABLE events AS ++ SELECT * ++ FROM read_parquet('{cleaned_data_path}') ++ """) ++ ++ # Compute page-level statistics ++ conn.execute(""" ++ CREATE TABLE page_stats AS ++ SELECT ++ page, ++ COUNT(*) as views, ++ COUNT(DISTINCT user_id) as unique_users, ++ ROUND(AVG(load_ms), 1) as avg_load_ms ++ FROM events ++ GROUP BY page ++ """) ++ ++ # Store result in temporary parquet ++ temp_file = PROJECT_ROOT / ".tmp" / f"{ds}_stats.parquet" ++ temp_file.parent.mkdir(parents=True, exist_ok=True) ++ ++ conn.execute(f"COPY page_stats TO '{temp_file}' (FORMAT PARQUET)") ++ conn.close() ++ ++ return str(temp_file) ++ ++ @task ++ def load(stats_path: str, ds=None) -> None: ++ """Write output CSV with columns: page, views, unique_users, avg_load_ms. ++ ++ Output is sorted by page. Reruns replace the day's file. ++ ++ Args: ++ stats_path: Path to aggregated statistics parquet file ++ ds: Airflow execution date (YYYY-MM-DD format) ++ """ ++ conn = duckdb.connect(":memory:") ++ ++ # Load stats and sort by page ++ conn.execute(f""" ++ CREATE TABLE stats AS ++ SELECT * ++ FROM read_parquet('{stats_path}') ++ ORDER BY page ++ """) ++ ++ # Create output directory ++ output_dir = PROJECT_ROOT / "output" / "page_stats" ++ output_dir.mkdir(parents=True, exist_ok=True) ++ output_file = output_dir / f"{ds}.csv" ++ ++ # Export to CSV ++ result = conn.execute("SELECT * FROM stats").fetchall() ++ columns = ["page", "views", "unique_users", "avg_load_ms"] ++ ++ with output_file.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(columns) ++ for row in result: ++ writer.writerow(row) ++ ++ conn.close() ++ ++ # Clean up temporary files ++ temp_dir = PROJECT_ROOT / ".tmp" ++ (temp_dir / f"{ds}_cleaned.parquet").unlink(missing_ok=True) ++ (temp_dir / f"{ds}_stats.parquet").unlink(missing_ok=True) ++ ++ # Task orchestration: extract → transform → load ++ cleaned_data = extract() ++ stats_data = transform(cleaned_data) ++ load(stats_data) ++ ++ ++clickstream_page_stats() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/attempt.json new file mode 100644 index 0000000..3918d93 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 207.3, + "cost_usd": 0.569764, + "cost_reported_usd": 1.968464, + "tokens": { + "input": 1398814, + "output": 15479, + "reasoning": 0, + "cache_read": 1092278, + "cache_write": 306422, + "total": 2812993 + }, + "steps": 21, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 21, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dac80a6ffeAxP2Jhf6Esly9U.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/grade.json new file mode 100644 index 0000000..1dd655a --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-1/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG clickstream_page_stats exists", + "kind": "primary", + "passed": true, + "detail": "found: ['clickstream_page_stats', 'marketing_spend_daily']" + }, + { + "name": "extract, transform and load are separate chained tasks", + "kind": "primary", + "passed": true, + "detail": "longest dependency chain: 3 tasks; deps=[['extract', 'transform'], ['transform', 'load']]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "page stats are correct for each day", + "kind": "primary", + "passed": true, + "detail": "2026-09-28: expected 32 pages, got 32 (first mismatch: none); 2026-09-29: expected 32 pages, got 32 (first mismatch: none)" + }, + { + "name": "no XCom/Variable value larger than 48 KB (data passed by reference)", + "kind": "primary", + "passed": true, + "detail": "4 XCom/Variable rows; largest: task=extract key=return_value bytes=170" + }, + { + "name": "rerun reproduces the output", + "kind": "primary", + "passed": true, + "detail": "rerun ok=True" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "21 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..c9acb63 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,161 @@ +diff --git a/dags/clickstream_page_stats.py b/dags/clickstream_page_stats.py +new file mode 100644 +index 0000000..c6c55bf +--- /dev/null ++++ b/dags/clickstream_page_stats.py +@@ -0,0 +1,155 @@ ++"""Daily page-level stats from the web clickstream exports. ++ ++Pipeline ++-------- ++extract → filter bot traffic (is_bot=1) and anonymous hits (empty user_id), ++ write a cleaned intermediate Parquet file (claim-check). ++transform → aggregate per page: views, unique_users, avg_load_ms (rounded to ++ 1 decimal). Uses DuckDB for columnar speed at 50x+ current volume. ++load → write output/page_stats/.csv atomically (temp-then-rename so ++ reruns are safe). ++ ++Claim-check note ++---------------- ++At ~20k events/day today and ~1 M events/day after the new tracker is live, ++passing cleaned data through XCom would bloat the Airflow metadata DB. Instead ++each task writes/reads a local Parquet file in output/.tmp/ and passes only the ++file *path* through XCom. ++""" ++ ++from __future__ import annotations ++ ++import os ++import tempfile ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++CLICKSTREAM_DIR = PROJECT_ROOT / "data" / "clickstream" ++OUTPUT_DIR = PROJECT_ROOT / "output" / "page_stats" ++TMP_DIR = PROJECT_ROOT / "output" / ".tmp" ++ ++ ++@dag( ++ dag_id="clickstream_page_stats", ++ schedule="@daily", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["clickstream", "web-analytics"], ++) ++def clickstream_page_stats(): ++ @task ++ def extract(ds: str | None = None) -> str: ++ """Read the day's gzipped export and drop bots + anonymous hits. ++ ++ Returns the path of a Parquet claim-check for the next task. ++ Raises FileNotFoundError if the export for *ds* does not exist. ++ """ ++ import duckdb ++ ++ src = CLICKSTREAM_DIR / f"{ds}.csv.gz" ++ if not src.exists(): ++ raise FileNotFoundError(f"No clickstream export found for {ds}: {src}") ++ ++ TMP_DIR.mkdir(parents=True, exist_ok=True) ++ claim_check = TMP_DIR / f"{ds}_extracted.parquet" ++ ++ con = duckdb.connect() ++ con.execute( ++ f""" ++ COPY ( ++ SELECT ++ event_id, ++ user_id, ++ page, ++ event_ts, ++ CAST(load_ms AS DOUBLE) AS load_ms ++ FROM read_csv_auto('{src}', compression='gzip') ++ WHERE CAST(is_bot AS INTEGER) = 0 ++ AND user_id IS NOT NULL ++ AND user_id != '' ++ ) TO '{claim_check}' (FORMAT PARQUET) ++ """ ++ ) ++ con.close() ++ return str(claim_check) ++ ++ @task ++ def transform(claim_check_path: str, ds: str | None = None) -> str: ++ """Aggregate per page and write a second Parquet claim-check. ++ ++ Computes: ++ views – row count per page ++ unique_users – COUNT(DISTINCT user_id) per page ++ avg_load_ms – ROUND(AVG(load_ms), 1) per page ++ """ ++ import duckdb ++ ++ src = claim_check_path ++ TMP_DIR.mkdir(parents=True, exist_ok=True) ++ aggregated = TMP_DIR / f"{ds}_aggregated.parquet" ++ ++ con = duckdb.connect() ++ con.execute( ++ f""" ++ COPY ( ++ SELECT ++ page, ++ COUNT(*) AS views, ++ COUNT(DISTINCT user_id) AS unique_users, ++ ROUND(AVG(load_ms), 1) AS avg_load_ms ++ FROM read_parquet('{src}') ++ GROUP BY page ++ ORDER BY page ++ ) TO '{aggregated}' (FORMAT PARQUET) ++ """ ++ ) ++ con.close() ++ return str(aggregated) ++ ++ @task ++ def load(claim_check_path: str, ds: str | None = None) -> None: ++ """Write output/page_stats/.csv from the aggregated Parquet. ++ ++ The write is atomic (temp file in the same directory + os.replace) so ++ that a partial run never leaves a corrupt file behind. A rerun simply ++ overwrites the previous day's output. ++ """ ++ import duckdb ++ ++ OUTPUT_DIR.mkdir(parents=True, exist_ok=True) ++ final = OUTPUT_DIR / f"{ds}.csv" ++ ++ # Write to a sibling temp file first, then atomically rename. ++ fd, tmp_path = tempfile.mkstemp( ++ dir=OUTPUT_DIR, prefix=f".{ds}_", suffix=".csv.tmp" ++ ) ++ try: ++ os.close(fd) ++ con = duckdb.connect() ++ con.execute( ++ f""" ++ COPY ( ++ SELECT page, views, unique_users, avg_load_ms ++ FROM read_parquet('{claim_check_path}') ++ ORDER BY page ++ ) TO '{tmp_path}' (FORMAT CSV, HEADER TRUE) ++ """ ++ ) ++ con.close() ++ os.replace(tmp_path, final) ++ except Exception: ++ try: ++ os.unlink(tmp_path) ++ except OSError: ++ pass ++ raise ++ ++ extracted = extract() ++ aggregated = transform(extracted) ++ load(aggregated) ++ ++ ++clickstream_page_stats() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..c11fcb2 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 124.6, + "cost_usd": 0.751673, + "cost_reported_usd": 2.77565, + "tokens": { + "input": 674670, + "output": 5275, + "reasoning": 0, + "cache_read": 538393, + "cache_write": 136266, + "total": 1354604 + }, + "steps": 10, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 4, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd65e3affezdP3xxVk0Ni2ey.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..ca50b23 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG clickstream_page_stats exists", + "kind": "primary", + "passed": true, + "detail": "found: ['clickstream_page_stats', 'marketing_spend_daily']" + }, + { + "name": "extract, transform and load are separate chained tasks", + "kind": "primary", + "passed": true, + "detail": "longest dependency chain: 3 tasks; deps=[['extract', 'transform'], ['transform', 'load']]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "page stats are correct for each day", + "kind": "primary", + "passed": true, + "detail": "2026-09-28: expected 32 pages, got 32 (first mismatch: none); 2026-09-29: expected 32 pages, got 32 (first mismatch: none)" + }, + { + "name": "no XCom/Variable value larger than 48 KB (data passed by reference)", + "kind": "primary", + "passed": true, + "detail": "4 XCom/Variable rows; largest: task=transform key=return_value bytes=180" + }, + { + "name": "rerun reproduces the output", + "kind": "primary", + "passed": true, + "detail": "rerun ok=True" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "4 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..3765bec --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch @@ -0,0 +1,192 @@ +diff --git a/dags/clickstream_page_stats.py b/dags/clickstream_page_stats.py +new file mode 100644 +index 0000000..b9e6054 +--- /dev/null ++++ b/dags/clickstream_page_stats.py +@@ -0,0 +1,186 @@ ++"""Daily page-level stats from web clickstream exports. ++ ++Pipeline: extract → transform → load ++- extract: read that day's gzipped export, drop bots (is_bot=1) and ++ anonymous hits (empty user_id), write a filtered CSV ++ claim-check so large payloads never touch XCom. ++- transform: aggregate per page: views, unique_users, avg_load_ms. ++- load: write output/page_stats/.csv sorted by page. ++ ++The claim-check pattern (filtered Parquet on disk, path in XCom) keeps ++memory flat as volume grows to the projected ~1 M events/day. ++""" ++ ++from __future__ import annotations ++ ++import csv ++import gzip ++import os ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++CLICKSTREAM_DIR = PROJECT_ROOT / "data" / "clickstream" ++STAGING_DIR = PROJECT_ROOT / "output" / "page_stats" / "_staging" ++OUTPUT_DIR = PROJECT_ROOT / "output" / "page_stats" ++ ++ ++@dag( ++ dag_id="clickstream_page_stats", ++ schedule="@daily", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["clickstream", "web"], ++) ++def clickstream_page_stats(): ++ @task ++ def extract(ds: str | None = None) -> str: ++ """Read that day's gzipped export, filter bots + anonymous, write ++ filtered rows to a staging CSV (claim-check) and return its path.""" ++ src = CLICKSTREAM_DIR / f"{ds}.csv.gz" ++ if not src.exists(): ++ raise FileNotFoundError( ++ f"Clickstream export not found: {src}. " ++ "Check that the export for this date has been delivered." ++ ) ++ ++ STAGING_DIR.mkdir(parents=True, exist_ok=True) ++ staging_path = STAGING_DIR / f"{ds}.csv" ++ ++ kept = 0 ++ dropped_bot = 0 ++ dropped_anon = 0 ++ ++ with ( ++ gzip.open(src, "rt", newline="") as src_fh, ++ staging_path.open("w", newline="") as dst_fh, ++ ): ++ reader = csv.DictReader(src_fh) ++ writer = csv.DictWriter( ++ dst_fh, ++ fieldnames=["user_id", "page", "load_ms"], ++ ) ++ writer.writeheader() ++ ++ for row in reader: ++ if row["is_bot"] == "1": ++ dropped_bot += 1 ++ continue ++ if not row["user_id"].strip(): ++ dropped_anon += 1 ++ continue ++ writer.writerow( ++ { ++ "user_id": row["user_id"], ++ "page": row["page"], ++ "load_ms": row["load_ms"], ++ } ++ ) ++ kept += 1 ++ ++ print( ++ f"[extract] ds={ds} kept={kept} " ++ f"dropped_bot={dropped_bot} dropped_anon={dropped_anon}" ++ ) ++ return str(staging_path) ++ ++ @task ++ def transform(staging_path: str) -> str: ++ """Aggregate filtered rows per page and write an aggregated CSV ++ (claim-check). Returns the path of the aggregated file.""" ++ src = Path(staging_path) ++ if not src.exists(): ++ raise FileNotFoundError(f"Staging file not found: {src}") ++ ++ # Accumulate per page ++ views: dict[str, int] = {} ++ users: dict[str, set] = {} ++ load_sum: dict[str, float] = {} ++ ++ with src.open(newline="") as fh: ++ for row in csv.DictReader(fh): ++ page = row["page"] ++ uid = row["user_id"] ++ load_ms_val = row["load_ms"] ++ ++ views[page] = views.get(page, 0) + 1 ++ ++ if page not in users: ++ users[page] = set() ++ users[page].add(uid) ++ ++ try: ++ ms = float(load_ms_val) ++ except ValueError: ++ ms = 0.0 ++ load_sum[page] = load_sum.get(page, 0.0) + ms ++ ++ # Write aggregated claim-check ++ agg_path = src.with_suffix(".agg.csv") ++ with agg_path.open("w", newline="") as fh: ++ writer = csv.DictWriter( ++ fh, fieldnames=["page", "views", "unique_users", "avg_load_ms"] ++ ) ++ writer.writeheader() ++ for page in views: ++ v = views[page] ++ u = len(users[page]) ++ avg = round(load_sum[page] / v, 1) if v else 0.0 ++ writer.writerow( ++ { ++ "page": page, ++ "views": v, ++ "unique_users": u, ++ "avg_load_ms": avg, ++ } ++ ) ++ ++ print(f"[transform] pages aggregated={len(views)}") ++ return str(agg_path) ++ ++ @task ++ def load(agg_path: str, ds: str | None = None) -> None: ++ """Sort aggregated rows by page and write the final output CSV. ++ Re-runs replace the existing file atomically.""" ++ src = Path(agg_path) ++ if not src.exists(): ++ raise FileNotFoundError(f"Aggregated file not found: {src}") ++ ++ rows: list[dict] = [] ++ with src.open(newline="") as fh: ++ rows = sorted(csv.DictReader(fh), key=lambda r: r["page"]) ++ ++ OUTPUT_DIR.mkdir(parents=True, exist_ok=True) ++ out = OUTPUT_DIR / f"{ds}.csv" ++ ++ # Write to a temp file then rename for atomicity (idempotent reruns) ++ tmp = out.with_suffix(".tmp") ++ with tmp.open("w", newline="") as fh: ++ writer = csv.DictWriter( ++ fh, fieldnames=["page", "views", "unique_users", "avg_load_ms"] ++ ) ++ writer.writeheader() ++ for row in rows: ++ writer.writerow(row) ++ tmp.replace(out) ++ ++ # Clean up staging files (best-effort) ++ # src is the .agg.csv; the raw filtered CSV is the same stem without .agg ++ try: ++ src.unlink(missing_ok=True) # e.g. _staging/2026-09-27.agg.csv ++ raw_csv = src.with_name(src.stem[:-4] + ".csv") # strip ".agg" → _staging/2026-09-27.csv ++ raw_csv.unlink(missing_ok=True) ++ except Exception: ++ pass # cleanup is best-effort ++ ++ print(f"[load] wrote {len(rows)} page rows → {out}") ++ ++ # Wire up the tasks; staging_path flows through XCom (just a short string) ++ staging_path = extract() ++ agg_path = transform(staging_path) ++ load(agg_path) ++ ++ ++clickstream_page_stats() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..162496a --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 175.1, + "cost_usd": 1.75752, + "cost_reported_usd": 6.145479, + "tokens": { + "input": 1462675, + "output": 7497, + "reasoning": 0, + "cache_read": 1113029, + "cache_write": 349624, + "total": 2932825 + }, + "steps": 21, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 9, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dad7bfbffeMEfR23VO4o3WkS.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json new file mode 100644 index 0000000..3a7c941 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-clickstream-claim-check/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG clickstream_page_stats exists", + "kind": "primary", + "passed": true, + "detail": "found: ['clickstream_page_stats', 'marketing_spend_daily']" + }, + { + "name": "extract, transform and load are separate chained tasks", + "kind": "primary", + "passed": true, + "detail": "longest dependency chain: 3 tasks; deps=[['extract', 'transform'], ['transform', 'load']]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "page stats are correct for each day", + "kind": "primary", + "passed": true, + "detail": "2026-09-28: expected 32 pages, got 32 (first mismatch: none); 2026-09-29: expected 32 pages, got 32 (first mismatch: none)" + }, + { + "name": "no XCom/Variable value larger than 48 KB (data passed by reference)", + "kind": "primary", + "passed": true, + "detail": "4 XCom/Variable rows; largest: task=transform key=return_value bytes=184" + }, + { + "name": "rerun reproduces the output", + "kind": "primary", + "passed": true, + "detail": "rerun ok=True" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "9 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..658c521 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,101 @@ +diff --git a/dags/daily_revenue.py b/dags/daily_revenue.py +new file mode 100644 +index 0000000..8245467 +--- /dev/null ++++ b/dags/daily_revenue.py +@@ -0,0 +1,95 @@ ++"""Generate daily revenue reports per store from the previous calendar day. ++ ++Runs at 03:00 UTC daily and reports on the previous calendar day (midnight to ++midnight UTC). Late orders trickle in until about 2am UTC, so the 03:00 run ++ensures all orders for a day are captured. ++ ++For example: The run on 2026-09-29 03:00 UTC reports on 2026-09-28 (all orders ++between 2026-09-28 00:00:00 and 2026-09-29 00:00:00 UTC). ++ ++Output: output/daily_revenue/.csv with columns: ++ - store_id ++ - orders (count of orders) ++ - revenue (sum of amounts, rounded to 2 decimals) ++ Sorted by store_id. ++""" ++ ++from __future__ import annotations ++ ++import pandas as pd ++import pendulum ++from airflow.sdk import dag, task ++from pathlib import Path ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="0 3 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["finance", "daily"], ++) ++def daily_revenue(): ++ """Generate daily revenue report for the previous calendar day.""" ++ ++ @task ++ def compute_daily_revenue(ds: str) -> str: ++ """ ++ Compute daily revenue for the previous calendar day. ++ ++ Args: ++ ds: The DAG run's logical date (YYYY-MM-DD). We report on the day ++ before this date (ds - 1 day). ++ ++ Returns: ++ Path to the output CSV file. ++ """ ++ # Parse the logical date and compute the report date (previous day) ++ logical_date = pendulum.parse(ds) ++ report_date = logical_date.subtract(days=1) ++ ++ # Define the time window: entire UTC day ++ window_start = report_date.start_of("day") ++ window_end = report_date.end_of("day").add(microseconds=1) ++ ++ # Load orders ++ orders_path = PROJECT_ROOT / "data" / "orders.csv" ++ df = pd.read_csv(orders_path) ++ ++ # Convert order_ts to datetime ++ df["order_ts"] = pd.to_datetime(df["order_ts"], utc=True) ++ ++ # Filter to the report day (previous calendar day, midnight to midnight UTC) ++ df_day = df[ ++ (df["order_ts"] >= window_start) & (df["order_ts"] < window_end) ++ ] ++ ++ # Aggregate by store_id ++ summary = ( ++ df_day.groupby("store_id", as_index=False) ++ .agg( ++ orders=("order_id", "count"), ++ revenue=("amount", "sum"), ++ ) ++ .sort_values("store_id") ++ .reset_index(drop=True) ++ ) ++ ++ # Round revenue to 2 decimals ++ summary["revenue"] = summary["revenue"].round(2) ++ ++ # Create output directory ++ output_dir = PROJECT_ROOT / "output" / "daily_revenue" ++ output_dir.mkdir(parents=True, exist_ok=True) ++ ++ # Write to CSV with the date (report_date) in the filename ++ output_path = output_dir / f"{report_date.format('YYYY-MM-DD')}.csv" ++ summary.to_csv(output_path, index=False) ++ ++ return str(output_path) ++ ++ compute_daily_revenue() ++ ++ ++daily_revenue() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..8a9e6f9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 1.0, + "returncode": 0, + "wall_s": 231.2, + "cost_usd": 0.66357, + "cost_reported_usd": 2.568192, + "tokens": { + "input": 1904772, + "output": 7621, + "reasoning": 0, + "cache_read": 1526489, + "cache_write": 378133, + "total": 3817015 + }, + "steps": 26, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 19, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd48961ffeeIiesd1Z6L8z43.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..1ae05fc --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,61 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG daily_revenue exists", + "kind": "primary", + "passed": true, + "detail": "found: ['daily_revenue', 'store_dimension']" + }, + { + "name": "scheduled runs fire daily at 03:00 UTC", + "kind": "primary", + "passed": true, + "detail": "[{\"start\": \"2026-10-01T03:00:00+00:00\", \"end\": \"2026-10-01T03:00:00+00:00\", \"run_after\": \"2026-10-01T03:00:00+00:00\"}, {\"start\": \"2026-10-02T03:00:00+00:00\", \"end\": \"2026-10-02T03:00:00+00:00\", \"run_after\": \"2026-10-02T03:00:00+00:00\"}, {\"start\": \"2026-10-03T03:00:00+00:00\", \"end\": \"2026-10-03T03:00:00+00:00\", \"run_after\": \"2026-10-03T03:00:00+00:00\"}]" + }, + { + "name": "scheduled runs execute successfully", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 1000.49), 'S02': (7, 989.79), 'S03': (8, 1028.89), 'S04': (5, 699.56)}" + }, + { + "name": "the 03:00 run on day D reports calendar day D-1 (and only that day)", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 1000.49), 'S02': (7, 989.79), 'S03': (8, 1028.89), 'S04': (5, 699.56)}" + }, + { + "name": "rerun of a scheduled run reproduces the same file", + "kind": "primary", + "passed": true, + "detail": "state=success; got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 1.0 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..3a43eda --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch @@ -0,0 +1,103 @@ +diff --git a/dags/daily_revenue.py b/dags/daily_revenue.py +new file mode 100644 +index 0000000..33b3980 +--- /dev/null ++++ b/dags/daily_revenue.py +@@ -0,0 +1,97 @@ ++"""Generate daily revenue report by store for the previous calendar day. ++ ++Runs daily at 03:00 UTC to capture all orders from the previous day ++(late orders trickle in until ~02:00 UTC). ++ ++Output: output/daily_revenue/.csv with columns: ++ - store_id ++ - orders (count) ++ - revenue (rounded to 2 decimals) ++ ++Sorted by store_id. Idempotent — can be safely rerun. ++""" ++ ++from __future__ import annotations ++ ++import csv ++from datetime import datetime, timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="0 3 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["finance", "revenue"], ++) ++def daily_revenue(): ++ """Daily revenue aggregation by store for the previous calendar day.""" ++ ++ @task ++ def generate_revenue_report(ds: str) -> str: ++ """ ++ Generate daily revenue report. ++ ++ Args: ++ ds: Logical date (YYYY-MM-DD) - the DAY BEING REPORTED ON. ++ When the DAG runs at 03:00 on 2026-09-29, ds='2026-09-28'. ++ ++ Returns: ++ Path to the generated CSV file. ++ """ ++ # Parse the logical date (the day we're reporting on) ++ report_date = datetime.strptime(ds, "%Y-%m-%d").date() ++ ++ # Read orders.csv and filter for the report date (midnight to midnight UTC) ++ orders_path = PROJECT_ROOT / "data" / "orders.csv" ++ daily_revenue_by_store = {} ++ ++ with open(orders_path, newline="") as f: ++ reader = csv.DictReader(f) ++ for row in reader: ++ # Parse order timestamp (UTC) ++ order_ts = datetime.strptime( ++ row["order_ts"], "%Y-%m-%d %H:%M:%S" ++ ).date() ++ ++ # Filter for the report date ++ if order_ts == report_date: ++ store_id = row["store_id"] ++ amount = float(row["amount"]) ++ ++ if store_id not in daily_revenue_by_store: ++ daily_revenue_by_store[store_id] = { ++ "orders": 0, ++ "revenue": 0.0, ++ } ++ ++ daily_revenue_by_store[store_id]["orders"] += 1 ++ daily_revenue_by_store[store_id]["revenue"] += amount ++ ++ # Sort by store_id for deterministic output ++ sorted_stores = sorted(daily_revenue_by_store.items()) ++ ++ # Write output ++ output_dir = PROJECT_ROOT / "output" / "daily_revenue" ++ output_dir.mkdir(parents=True, exist_ok=True) ++ output_file = output_dir / f"{ds}.csv" ++ ++ with open(output_file, "w", newline="") as f: ++ writer = csv.writer(f) ++ writer.writerow(["store_id", "orders", "revenue"]) ++ for store_id, stats in sorted_stores: ++ # Round revenue to 2 decimals ++ revenue = round(stats["revenue"], 2) ++ writer.writerow([store_id, stats["orders"], revenue]) ++ ++ return str(output_file) ++ ++ generate_revenue_report() ++ ++ ++daily_revenue() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..6614975 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "task_fail", + "reason": "failed primary: the 03:00 run on day D reports calendar day D-1 (and only that day), rerun of a scheduled run reproduces the same file", + "primary_pass": false, + "primary_score": 0.6666666666666666, + "secondary_score": 1.0, + "returncode": 0, + "wall_s": 235.5, + "cost_usd": 0.739199, + "cost_reported_usd": 3.305479, + "tokens": { + "input": 2566481, + "output": 8912, + "reasoning": 0, + "cache_read": 2185576, + "cache_write": 380704, + "total": 5141673 + }, + "steps": 35, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 28, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dabbb40ffeQZel2S2G84JvSB.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json new file mode 100644 index 0000000..14fad70 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json @@ -0,0 +1,61 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG daily_revenue exists", + "kind": "primary", + "passed": true, + "detail": "found: ['daily_revenue', 'store_dimension']" + }, + { + "name": "scheduled runs fire daily at 03:00 UTC", + "kind": "primary", + "passed": true, + "detail": "[{\"start\": \"2026-10-01T03:00:00+00:00\", \"end\": \"2026-10-01T03:00:00+00:00\", \"run_after\": \"2026-10-01T03:00:00+00:00\"}, {\"start\": \"2026-10-02T03:00:00+00:00\", \"end\": \"2026-10-02T03:00:00+00:00\", \"run_after\": \"2026-10-02T03:00:00+00:00\"}, {\"start\": \"2026-10-03T03:00:00+00:00\", \"end\": \"2026-10-03T03:00:00+00:00\", \"run_after\": \"2026-10-03T03:00:00+00:00\"}]" + }, + { + "name": "scheduled runs execute successfully", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-29.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got=missing\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-26.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got=missing" + }, + { + "name": "the 03:00 run on day D reports calendar day D-1 (and only that day)", + "kind": "primary", + "passed": false, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-29.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got=missing\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-26.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got=missing" + }, + { + "name": "rerun of a scheduled run reproduces the same file", + "kind": "primary", + "passed": false, + "detail": "state=success; got=missing" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.6666666666666666, + "secondary_score": 1.0 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..331ec14 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,78 @@ +diff --git a/dags/daily_revenue.py b/dags/daily_revenue.py +new file mode 100644 +index 0000000..d60d6df +--- /dev/null ++++ b/dags/daily_revenue.py +@@ -0,0 +1,72 @@ ++"""Daily revenue report per store for the finance team. ++ ++Runs every day at 03:00 UTC and reports on the previous calendar day ++(midnight-to-midnight UTC). Late orders trickle in until ~02:00 UTC, so a ++03:00 run safely captures all orders for the preceding day. ++ ++Output: output/daily_revenue/.csv ++Columns: store_id, orders, revenue (revenue rounded to 2 d.p., sorted by store_id) ++ ++Idempotent: writing to a fixed filename for the reporting date means re-runs ++overwrite the previous file with identical content. ++""" ++ ++from __future__ import annotations ++ ++import csv ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DATA_FILE = PROJECT_ROOT / "data" / "orders.csv" ++ ++ ++@dag( ++ schedule="0 3 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["finance", "revenue"], ++) ++def daily_revenue(): ++ @task ++ def generate_report(logical_date=None) -> str: ++ """Aggregate orders for the previous calendar day and write the CSV.""" ++ import duckdb ++ ++ # The DAG runs at 03:00 on day D and reports on day D-1. ++ reporting_date = logical_date.date() - timedelta(days=1) ++ day_start = str(reporting_date) # YYYY-MM-DD 00:00:00 ++ day_end = str(reporting_date + timedelta(days=1)) # next day 00:00:00 ++ ++ out_dir = PROJECT_ROOT / "output" / "daily_revenue" ++ out_dir.mkdir(parents=True, exist_ok=True) ++ out_file = out_dir / f"{reporting_date}.csv" ++ ++ rows = duckdb.sql( ++ f""" ++ SELECT ++ store_id, ++ COUNT(*) AS orders, ++ ROUND(SUM(amount), 2) AS revenue ++ FROM read_csv_auto('{DATA_FILE}', header = true) ++ WHERE order_ts >= TIMESTAMPTZ '{day_start}' ++ AND order_ts < TIMESTAMPTZ '{day_end}' ++ GROUP BY store_id ++ ORDER BY store_id ++ """ ++ ).fetchall() ++ ++ with out_file.open("w", newline="") as fh: ++ writer = csv.writer(fh) ++ writer.writerow(["store_id", "orders", "revenue"]) ++ writer.writerows(rows) ++ ++ return str(out_file) ++ ++ generate_report() ++ ++ ++daily_revenue() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..1479a91 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,38 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 126.7, + "cost_usd": 1.243521, + "cost_reported_usd": 3.492666, + "tokens": { + "input": 749727, + "output": 5462, + "reasoning": 0, + "cache_read": 478225, + "cache_write": 271490, + "total": 1504904 + }, + "steps": 11, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [ + { + "name": "creating-dbt-models", + "origin": "global", + "status": "completed" + } + ], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 3, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd4b83effezxyqVZ55IsPvt5.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..e95c0af --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,61 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG daily_revenue exists", + "kind": "primary", + "passed": true, + "detail": "found: ['daily_revenue', 'store_dimension']" + }, + { + "name": "scheduled runs fire daily at 03:00 UTC", + "kind": "primary", + "passed": true, + "detail": "[{\"start\": \"2026-10-01T03:00:00+00:00\", \"end\": \"2026-10-01T03:00:00+00:00\", \"run_after\": \"2026-10-01T03:00:00+00:00\"}, {\"start\": \"2026-10-02T03:00:00+00:00\", \"end\": \"2026-10-02T03:00:00+00:00\", \"run_after\": \"2026-10-02T03:00:00+00:00\"}, {\"start\": \"2026-10-03T03:00:00+00:00\", \"end\": \"2026-10-03T03:00:00+00:00\", \"run_after\": \"2026-10-03T03:00:00+00:00\"}]" + }, + { + "name": "scheduled runs execute successfully", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 1000.49), 'S02': (7, 989.79), 'S03': (8, 1028.89), 'S04': (5, 699.56)}" + }, + { + "name": "the 03:00 run on day D reports calendar day D-1 (and only that day)", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 1000.49), 'S02': (7, 989.79), 'S03': (8, 1028.89), 'S04': (5, 699.56)}" + }, + { + "name": "rerun of a scheduled run reproduces the same file", + "kind": "primary", + "passed": true, + "detail": "state=success; got={'S01': (7, 1088.11), 'S02': (9, 1319.12), 'S03': (7, 1011.68), 'S04': (9, 1065.68)}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "3 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..637ff2d --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch @@ -0,0 +1,76 @@ +diff --git a/dags/daily_revenue.py b/dags/daily_revenue.py +new file mode 100644 +index 0000000..2bb9576 +--- /dev/null ++++ b/dags/daily_revenue.py +@@ -0,0 +1,70 @@ ++"""Daily revenue per store, reported for the previous calendar day (UTC). ++ ++Finance picks up output/daily_revenue/.csv every morning. ++The DAG runs at 03:00 UTC so that late orders (which trickle in until ~02:00) ++are included. The run that starts on the 29th reports on the 28th. ++ ++Re-running any day produces the same file (idempotent). ++Catchup is enabled so every day since 2026-09-01 can be rerun. ++""" ++ ++from __future__ import annotations ++ ++import datetime ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="0 3 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["finance", "revenue"], ++) ++def daily_revenue(): ++ @task ++ def generate_report(logical_date=None) -> str: ++ import duckdb ++ ++ # The run at 03:00 on day D reports on day D-1. ++ run_date: datetime.date = logical_date.date() ++ report_date: datetime.date = run_date - datetime.timedelta(days=1) ++ ++ day_start = datetime.datetime( ++ report_date.year, report_date.month, report_date.day, ++ 0, 0, 0, tzinfo=datetime.timezone.utc, ++ ) ++ day_end = day_start + datetime.timedelta(days=1) ++ ++ orders_path = str(PROJECT_ROOT / "data" / "orders.csv") ++ ++ sql = """ ++ SELECT ++ store_id, ++ COUNT(*) AS orders, ++ ROUND(SUM(amount), 2) AS revenue ++ FROM read_csv_auto(?) ++ WHERE order_ts >= ? ++ AND order_ts < ? ++ GROUP BY store_id ++ ORDER BY store_id ++ """ ++ ++ result = duckdb.execute(sql, [orders_path, day_start, day_end]).fetchdf() ++ ++ out_dir = PROJECT_ROOT / "output" / "daily_revenue" ++ out_dir.mkdir(parents=True, exist_ok=True) ++ ++ out_path = out_dir / f"{report_date}.csv" ++ result.to_csv(out_path, index=False) ++ ++ return str(out_path) ++ ++ generate_report() ++ ++ ++daily_revenue() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..203f5d1 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json @@ -0,0 +1,38 @@ +{ + "status": "task_fail", + "reason": "failed primary: the 03:00 run on day D reports calendar day D-1 (and only that day), rerun of a scheduled run reproduces the same file", + "primary_pass": false, + "primary_score": 0.6666666666666666, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 118.6, + "cost_usd": 1.116698, + "cost_reported_usd": 4.175702, + "tokens": { + "input": 1019683, + "output": 4884, + "reasoning": 0, + "cache_read": 805902, + "cache_write": 213766, + "total": 2044235 + }, + "steps": 14, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [ + { + "name": "dbt-develop", + "origin": "builtin", + "status": "completed" + } + ], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 6, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dac6b9bffex6LTTutrtExlrr.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json new file mode 100644 index 0000000..73273fe --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-daily-revenue-yesterday/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json @@ -0,0 +1,61 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG daily_revenue exists", + "kind": "primary", + "passed": true, + "detail": "found: ['daily_revenue', 'store_dimension']" + }, + { + "name": "scheduled runs fire daily at 03:00 UTC", + "kind": "primary", + "passed": true, + "detail": "[{\"start\": \"2026-10-01T03:00:00+00:00\", \"end\": \"2026-10-01T03:00:00+00:00\", \"run_after\": \"2026-10-01T03:00:00+00:00\"}, {\"start\": \"2026-10-02T03:00:00+00:00\", \"end\": \"2026-10-02T03:00:00+00:00\", \"run_after\": \"2026-10-02T03:00:00+00:00\"}, {\"start\": \"2026-10-03T03:00:00+00:00\", \"end\": \"2026-10-03T03:00:00+00:00\", \"run_after\": \"2026-10-03T03:00:00+00:00\"}]" + }, + { + "name": "scheduled runs execute successfully", + "kind": "primary", + "passed": true, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (8, 1067.6), 'S02': (8, 1027.73), 'S03': (8, 1206.03), 'S04': (13, 1233.89)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 734.21), 'S02': (3, 241.14), 'S03': (8, 951.64), 'S04': (6, 600.37)}" + }, + { + "name": "the 03:00 run on day D reports calendar day D-1 (and only that day)", + "kind": "primary", + "passed": false, + "detail": "run firing 2026-09-29T03:00:00+00:00: state=success logical_date=2026-09-29T03:00:00+00:00 new files=['2026-09-28.csv'] expected 2026-09-28.csv={'S04': (9, 1065.68), 'S01': (7, 1088.11), 'S03': (7, 1011.68), 'S02': (9, 1319.12)} got={'S01': (8, 1067.6), 'S02': (8, 1027.73), 'S03': (8, 1206.03), 'S04': (13, 1233.89)}\nrun firing 2026-09-26T03:00:00+00:00: state=success logical_date=2026-09-26T03:00:00+00:00 new files=['2026-09-25.csv'] expected 2026-09-25.csv={'S01': (5, 1000.49), 'S04': (5, 699.56), 'S03': (8, 1028.89), 'S02': (7, 989.79)} got={'S01': (5, 734.21), 'S02': (3, 241.14), 'S03': (8, 951.64), 'S04': (6, 600.37)}" + }, + { + "name": "rerun of a scheduled run reproduces the same file", + "kind": "primary", + "passed": false, + "detail": "state=success; got={'S01': (8, 1067.6), 'S02': (8, 1027.73), 'S03': (8, 1206.03), 'S04': (13, 1233.89)}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "6 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.6666666666666666, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..2ea4077 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,133 @@ +diff --git a/dags/partner_feeds.py b/dags/partner_feeds.py +new file mode 100644 +index 0000000..3517cbe +--- /dev/null ++++ b/dags/partner_feeds.py +@@ -0,0 +1,127 @@ ++"""Daily DAG for processing partner order files. ++ ++Each partner's CSV file is processed as its own task instance, so failures ++in one partner's file don't block others (isolation via task-level retries). ++Output is written as JSON per partner, with a summary written for each date. ++""" ++ ++from __future__ import annotations ++ ++import json ++from pathlib import Path ++from decimal import Decimal ++ ++import pandas as pd ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="0 1 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["partners"], ++) ++def partner_feeds(): ++ """Process daily partner order files and generate summary.""" ++ ++ @task ++ def discover_files(run_date: str) -> list[dict]: ++ """Discover all CSV files for the given date. ++ ++ Returns list of dicts with partner name and run_date for expansion. ++ Returns empty list if date folder doesn't exist. ++ """ ++ date_folder = PROJECT_ROOT / "data" / "partner_drop" / run_date ++ if not date_folder.exists(): ++ return [] ++ ++ # Get all .csv files and return dicts with partner name and run_date ++ partners = sorted([f.stem for f in date_folder.glob("*.csv")]) ++ return [{"partner": p, "run_date": run_date} for p in partners] ++ ++ @task ++ def process_file(partner: str, run_date: str) -> dict: ++ """Process a single partner's CSV file. ++ ++ Each call to this task is a separate task instance, allowing ++ individual files to fail and retry without blocking others. ++ """ ++ csv_path = PROJECT_ROOT / "data" / "partner_drop" / run_date / f"{partner}.csv" ++ ++ # Read CSV ++ df = pd.read_csv(csv_path) ++ row_count = len(df) ++ ++ # Sum amount with 2 decimal places (using Decimal for precision) ++ total = Decimal(0) ++ for amount in df["amount"]: ++ total += Decimal(str(amount)) ++ ++ # Round to 2 decimal places ++ total_amount = round(float(total), 2) ++ ++ # Write output JSON ++ output_dir = PROJECT_ROOT / "output" / "partner_feeds" / run_date ++ output_dir.mkdir(parents=True, exist_ok=True) ++ output_path = output_dir / f"{partner}.json" ++ ++ output_data = { ++ "partner": partner, ++ "rows": row_count, ++ "total_amount": total_amount, ++ } ++ output_path.write_text(json.dumps(output_data)) ++ ++ return { ++ "partner": partner, ++ "rows": row_count, ++ "total_amount": total_amount, ++ } ++ ++ @task ++ def write_summary(partner_results: list[dict], run_date: str) -> str: ++ """Write summary JSON after all files are processed. ++ ++ Summary is written even if no files exist (all zeros). ++ """ ++ output_dir = PROJECT_ROOT / "output" / "partner_feeds" / run_date ++ output_dir.mkdir(parents=True, exist_ok=True) ++ summary_path = output_dir / "summary.json" ++ ++ # Calculate totals ++ file_count = len(partner_results) ++ total_rows = sum(r["rows"] for r in partner_results) ++ total_amount = sum(r["total_amount"] for r in partner_results) ++ total_amount = round(total_amount, 2) ++ ++ summary_data = { ++ "files": file_count, ++ "rows": total_rows, ++ "total_amount": total_amount, ++ } ++ summary_path.write_text(json.dumps(summary_data)) ++ ++ return str(summary_path) ++ ++ # DAG flow ++ # Step 1: Discover files for this date ++ partners = discover_files(run_date="{{ ds }}") ++ ++ # Step 2: Process each partner file with dynamic task mapping ++ # process_file.expand() creates a separate task instance for each partner ++ # This ensures individual file failures only retry that specific task ++ # Note: only 'partner' is expanded (creates one task per partner). ++ # 'run_date' is passed as a constant to all task instances. ++ process_results = process_file.expand( ++ partner=partners, ++ ).set_kwargs(run_date="{{ ds }}") ++ ++ # Step 3: Write summary after all files are processed ++ write_summary(partner_results=process_results, run_date="{{ ds }}") ++ ++ ++# Instantiate the DAG ++partner_feeds() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..d543081 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "turn_limit", + "reason": "max turns reached", + "primary_pass": false, + "primary_score": 0.0, + "secondary_score": 1.0, + "returncode": 1, + "wall_s": 311.8, + "cost_usd": 0.855348, + "cost_reported_usd": 3.721205, + "tokens": { + "input": 2866080, + "output": 20856, + "reasoning": 0, + "cache_read": 2462153, + "cache_write": 403704, + "total": 5752793 + }, + "steps": 42, + "termination": { + "why_model_stopped": "tool-call", + "why_harness_stopped": "budget-exhausted", + "done_reason": "none" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 23, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd22c15ffeKWRQKQlDt21HTv.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..88316b7 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,37 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": false, + "detail": "dags/partner_feeds.py: AttributeError: 'PlainXComArg' object has no attribute 'set_kwargs'" + }, + { + "name": "DAG partner_feeds exists", + "kind": "primary", + "passed": false, + "detail": "found: ['partner_registry_export']" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.0, + "secondary_score": 1.0 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..60ca9a1 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch @@ -0,0 +1,298 @@ +diff --git a/README.md b/README.md +index 40ae439..67c3182 100644 +--- a/README.md ++++ b/README.md +@@ -9,3 +9,129 @@ Airflow 3.3 DAGs that ingest data sent to us by marketplace partners. + - `output/`: DAG outputs + + Run a DAG locally with `airflow dags test `. ++ ++## DAG: `partner_feeds` ++ ++Daily Airflow DAG that processes partner order files and produces aggregated JSON outputs for finance reporting. ++ ++### Overview ++ ++- **Schedule**: Daily at 01:00 UTC ++- **Start Date**: 2026-09-01 ++- **Catchup**: Enabled (can rerun any historical date) ++- **Tags**: `partners` ++ ++### Inputs ++ ++Partner CSV files are expected in: ++``` ++data/partner_drop//.csv ++``` ++ ++Each file contains: ++``` ++order_id,amount ++GLO-20260903-000,53.24 ++GLO-20260903-001,72.30 ++... ++``` ++ ++### Outputs ++ ++For each date run, the DAG produces: ++ ++**Individual partner outputs:** ++``` ++output/partner_feeds//.json ++``` ++ ++Example: ++```json ++{ ++ "partner": "acme", ++ "rows": 25, ++ "total_amount": 7502.67 ++} ++``` ++ ++**Daily summary:** ++``` ++output/partner_feeds//summary.json ++``` ++ ++Example: ++```json ++{ ++ "files": 4, ++ "rows": 50, ++ "total_amount": 14467.21 ++} ++``` ++ ++### Behavior ++ ++1. **Discovers files** — Scans `data/partner_drop//` for all `*.csv` files ++2. **Process each file independently** — Each partner's CSV is processed by its own task instance (using Airflow task mapping) ++3. **Calculate aggregates** — For each file: ++ - Count rows (excluding header) ++ - Sum amounts to 2 decimal places ++ - Write `.json` ++4. **Write summary** — After all files are processed: ++ - Aggregate counts and totals across all partners ++ - Write `summary.json` (always written, even if no files exist) ++ ++### Failure Isolation ++ ++Because each partner file is processed by its own mapped task instance: ++- If `acme.csv` is corrupt, only that task fails and retries ++- `globex.csv`, `initech.csv`, etc. continue processing ++- Finance still gets a `summary.json` reflecting the successfully processed files ++ ++### Empty Dates ++ ++If a date has no partner files (folder doesn't exist or is empty): ++- No individual `.json` files are written ++- A `summary.json` is still written with all zeros: ++ ```json ++ { ++ "files": 0, ++ "rows": 0, ++ "total_amount": 0.0 ++ } ++ ``` ++ ++This ensures finance can rely on checking `summary.json` every morning. ++ ++### Precision ++ ++Amounts are: ++- Read as strings from CSV ++- Converted to Python `Decimal` for exact arithmetic (no float rounding) ++- Rounded to 2 decimal places in output JSON ++- Serialized as floats in the JSON (e.g., `7502.67` not `"7502.67"`) ++ ++### Backfilling ++ ++To rerun a historical date: ++```bash ++airflow dags backfill partner_feeds \ ++ --start-date 2026-09-15 \ ++ --end-date 2026-09-15 ++``` ++ ++Or trigger from the UI. The DAG supports any date from 2026-09-01 onwards. ++ ++### Task Mapping Example ++ ++For a date with files: `acme.csv`, `globex.csv`, `initech.csv`: ++ ++``` ++discover_files[1] ++ ├─→ process_file[0] (acme) → output/acme.json ++ ├─→ process_file[1] (globex) → output/globex.json ++ ├─→ process_file[2] (initech) → output/initech.json ++ └─→ write_summary ────────────→ output/summary.json ++``` ++ ++Each `process_file[i]` task is independent. If any task fails, `write_summary` waits for all to complete (or timeout). ++ +diff --git a/dags/partner_feeds.py b/dags/partner_feeds.py +new file mode 100644 +index 0000000..e717584 +--- /dev/null ++++ b/dags/partner_feeds.py +@@ -0,0 +1,158 @@ ++""" ++Daily partner feed processor. ++ ++Ingests partner order files from data/partner_drop//.csv ++and writes aggregated outputs to output/partner_feeds//. ++ ++Each partner file is processed independently (task mapping) so a broken file ++only fails its own task, not the entire date. A summary.json is always written, ++even on days with no files. ++""" ++ ++from __future__ import annotations ++ ++import csv ++import json ++from decimal import Decimal ++from pathlib import Path ++from typing import Any ++ ++import pendulum ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ schedule="0 1 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["partners"], ++) ++def partner_feeds(): ++ """ ++ Daily DAG to process partner order files. ++ ++ - Discovers all .csv files in data/partner_drop// ++ - Maps each file to an independent task (failures don't cascade) ++ - Writes output/partner_feeds//.json for each file ++ - Aggregates to output/partner_feeds//summary.json (always written) ++ """ ++ ++ @task ++ def discover_files(**context) -> list[dict[str, str]]: ++ """ ++ Discover all partner CSV files for the given date. ++ ++ Returns a list of dicts suitable for task_mapping: ++ [{"partner": "acme", "filepath": "/path/to/acme.csv"}, ...] ++ ++ If the date folder doesn't exist, returns empty list. ++ """ ++ ds = context["ds"] # Logical date (YYYY-MM-DD) ++ date_folder = PROJECT_ROOT / "data" / "partner_drop" / ds ++ ++ if not date_folder.exists(): ++ return [] ++ ++ files = [] ++ for csv_file in sorted(date_folder.glob("*.csv")): ++ partner_name = csv_file.stem # filename without .csv ++ files.append({ ++ "partner": partner_name, ++ "filepath": str(csv_file), ++ }) ++ ++ return files ++ ++ @task ++ def process_file(partner: str, filepath: str, **context) -> dict[str, Any]: ++ """ ++ Process a single partner CSV file. ++ ++ Reads CSV, counts rows, sums amounts, and writes output JSON. ++ Returns metadata for summary aggregation. ++ ++ Args: ++ partner: Partner name (e.g., "acme") ++ filepath: Absolute path to partner CSV file ++ ++ Returns: ++ dict with keys: partner, rows, total_amount ++ """ ++ ds = context["ds"] # Logical date (YYYY-MM-DD) ++ row_count = 0 ++ total_amount = Decimal("0.00") ++ ++ # Read the CSV and aggregate ++ with open(filepath, "r") as f: ++ reader = csv.DictReader(f) ++ for row in reader: ++ row_count += 1 ++ # amount is a string like "53.24", convert to Decimal for precision ++ total_amount += Decimal(row["amount"]) ++ ++ # Write individual partner output ++ out_dir = PROJECT_ROOT / "output" / "partner_feeds" / ds ++ out_dir.mkdir(parents=True, exist_ok=True) ++ out_file = out_dir / f"{partner}.json" ++ ++ output = { ++ "partner": partner, ++ "rows": row_count, ++ "total_amount": float(total_amount.quantize(Decimal("0.01"))), ++ } ++ ++ out_file.write_text(json.dumps(output, indent=2)) ++ ++ return output ++ ++ @task ++ def write_summary(file_results: list[dict[str, Any]], **context) -> str: ++ """ ++ Aggregate results from all partner files and write summary.json. ++ ++ Called even if file_results is empty (no files that day). ++ Always writes summary.json with all zeros if needed. ++ ++ Args: ++ file_results: List of dicts from process_file() task ++ ++ Returns: ++ Path to the summary file written ++ """ ++ ds = context["ds"] # Logical date (YYYY-MM-DD) ++ total_files = len(file_results) ++ total_rows = sum(r["rows"] for r in file_results) ++ # Explicitly initialize to Decimal("0.00") to handle empty file_results ++ total_amount = sum( ++ (Decimal(str(r["total_amount"])) for r in file_results), ++ Decimal("0.00") ++ ) ++ ++ summary = { ++ "files": total_files, ++ "rows": total_rows, ++ "total_amount": float(total_amount.quantize(Decimal("0.01"))), ++ } ++ ++ out_dir = PROJECT_ROOT / "output" / "partner_feeds" / ds ++ out_dir.mkdir(parents=True, exist_ok=True) ++ summary_file = out_dir / "summary.json" ++ ++ summary_file.write_text(json.dumps(summary, indent=2)) ++ ++ return str(summary_file) ++ ++ # Task execution ++ discovered = discover_files() ++ ++ # Dynamic task mapping: process each file independently ++ # expand_kwargs unpacks each dict in the list as kwargs to process_file ++ results = process_file.expand_kwargs(discovered) ++ ++ # After all files processed (or empty list), write summary ++ write_summary(file_results=results) ++ ++ ++partner_feeds() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..b74241c --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "turn_limit", + "reason": "max turns reached", + "primary_pass": false, + "primary_score": 0.8888888888888888, + "secondary_score": 0.6666666666666666, + "returncode": 1, + "wall_s": 289.7, + "cost_usd": 0.852317, + "cost_reported_usd": 3.738099, + "tokens": { + "input": 2886009, + "output": 19816, + "reasoning": 0, + "cache_read": 2481928, + "cache_write": 403854, + "total": 5791607 + }, + "steps": 42, + "termination": { + "why_model_stopped": "tool-call", + "why_harness_stopped": "budget-exhausted", + "done_reason": "none" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 26, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0da9e51cffehej4cpO081bNXv.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json new file mode 100644 index 0000000..9e2ad53 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json @@ -0,0 +1,79 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feeds exists", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_feeds', 'partner_registry_export']" + }, + { + "name": "task set is independent of files present at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"fixture data\": [\"discover_files\", \"process_file\", \"write_summary\"], \"no partner_drop dir\": [\"discover_files\", \"process_file\", \"write_summary\"], \"extra files\": [\"discover_files\", \"process_file\", \"write_summary\"]}" + }, + { + "name": "uses dynamic task mapping", + "kind": "primary", + "passed": true, + "detail": "mapped tasks: ['process_file']" + }, + { + "name": "airflow dags test succeeds for 0, 1 and N files", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "one mapped task instance per file (0/1/N)", + "kind": "primary", + "passed": true, + "detail": "expected {'2026-09-01': 0, '2026-09-02': 1, '2026-09-03': 4}; expanded instances per mapped task: {'2026-09-01': {}, '2026-09-02': {'process_file': 1}, '2026-09-03': {'process_file': 4}}" + }, + { + "name": "per-partner result files are correct", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "summary is correct, including the day with no files", + "kind": "primary", + "passed": false, + "detail": "2026-09-01: summary expected {'files': 0, 'rows': 0, 'total_amount': 0} got None" + }, + { + "name": "rerun of a day reproduces the same outputs", + "kind": "primary", + "passed": true, + "detail": "2026-09-03: summary expected {'files': 4, 'rows': 30, 'total_amount': 7988.86} got {'files': 4, 'rows': 30, 'total_amount': 7988.86}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "26 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.8888888888888888, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..e6b0e01 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,126 @@ +diff --git a/dags/partner_feeds.py b/dags/partner_feeds.py +new file mode 100644 +index 0000000..bcb578d +--- /dev/null ++++ b/dags/partner_feeds.py +@@ -0,0 +1,120 @@ ++"""Daily DAG that processes partner order-feed CSV files. ++ ++Layout ++------ ++ data/partner_drop//.csv (columns: order_id, amount) ++ ++For each file it writes: ++ output/partner_feeds//.json ++ {"partner": ..., "rows": , "total_amount": } ++ ++After all per-partner tasks it writes (even on days with zero files): ++ output/partner_feeds//summary.json ++ {"files": , "rows": , "total_amount": } ++ ++A broken file only fails (and retries) its own task instance; all other ++partners keep running. Re-runs are safe: output files are overwritten. ++""" ++ ++from __future__ import annotations ++ ++import csv ++import json ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++from airflow.task.trigger_rule import TriggerRule ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++ ++ ++@dag( ++ dag_id="partner_feeds", ++ schedule="@daily", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ tags=["partners"], ++ default_args={"retries": 3}, ++) ++def partner_feeds(): ++ """Process per-partner CSV drops and write per-file + summary JSON.""" ++ ++ # ------------------------------------------------------------------ ++ # 1. Discover which partners sent files for this logical date. ++ # Returns a list of {"partner": , "ds": } dicts so ++ # expand_kwargs() can fan them out without touching context vars. ++ # ------------------------------------------------------------------ ++ @task ++ def list_partners(ds: str = None) -> list[dict]: ++ drop_dir = PROJECT_ROOT / "data" / "partner_drop" / ds ++ if not drop_dir.exists(): ++ return [] ++ return [ ++ {"partner": p.stem, "ds": ds} ++ for p in sorted(drop_dir.glob("*.csv")) ++ ] ++ ++ # ------------------------------------------------------------------ ++ # 2. Process one partner file. Called once per mapped value. ++ # Returns a dict so the summary task can aggregate without re-reading. ++ # ------------------------------------------------------------------ ++ @task ++ def process_partner(partner: str, ds: str) -> dict: ++ csv_path = PROJECT_ROOT / "data" / "partner_drop" / ds / f"{partner}.csv" ++ ++ rows = 0 ++ total_amount = 0.0 ++ with csv_path.open(newline="") as fh: ++ reader = csv.DictReader(fh) ++ for row in reader: ++ rows += 1 ++ total_amount += float(row["amount"]) ++ ++ result = { ++ "partner": partner, ++ "rows": rows, ++ "total_amount": round(total_amount, 2), ++ } ++ ++ out_path = PROJECT_ROOT / "output" / "partner_feeds" / ds / f"{partner}.json" ++ out_path.parent.mkdir(parents=True, exist_ok=True) ++ out_path.write_text(json.dumps(result)) ++ ++ return result ++ ++ # ------------------------------------------------------------------ ++ # 3. Write the daily summary. ++ # trigger_rule=ALL_DONE ensures this runs even if some partner tasks ++ # failed, and even when there are zero partners (empty list → zero ++ # mapped instances → downstream still fires). ++ # ------------------------------------------------------------------ ++ @task(trigger_rule=TriggerRule.ALL_DONE) ++ def write_summary(partner_results: list[dict], ds: str = None) -> None: ++ # partner_results contains one dict per successfully processed partner; ++ # failed mapped instances contribute None, so we filter those out. ++ valid = [r for r in (partner_results or []) if r is not None] ++ ++ summary = { ++ "files": len(valid), ++ "rows": sum(r["rows"] for r in valid), ++ "total_amount": round(sum(r["total_amount"] for r in valid), 2), ++ } ++ ++ out_path = PROJECT_ROOT / "output" / "partner_feeds" / ds / "summary.json" ++ out_path.parent.mkdir(parents=True, exist_ok=True) ++ out_path.write_text(json.dumps(summary)) ++ ++ # ------------------------------------------------------------------ ++ # Wire tasks together ++ # ------------------------------------------------------------------ ++ partners = list_partners() ++ ++ # expand_kwargs() creates one task instance per {"partner": ..., "ds": ...} ++ # dict — full isolation: a broken file only fails its own instance. ++ per_partner = process_partner.expand_kwargs(partners) ++ ++ write_summary(partner_results=per_partner) ++ ++ ++partner_feeds() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..75a6f29 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 197.1, + "cost_usd": 1.718919, + "cost_reported_usd": 5.543784, + "tokens": { + "input": 1274974, + "output": 7651, + "reasoning": 0, + "cache_read": 920865, + "cache_write": 354090, + "total": 2557580 + }, + "steps": 18, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 12, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd3c439ffewlelwExv5Te0TD.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..30e3edf --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,79 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feeds exists", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_feeds', 'partner_registry_export']" + }, + { + "name": "task set is independent of files present at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"fixture data\": [\"list_partners\", \"process_partner\", \"write_summary\"], \"no partner_drop dir\": [\"list_partners\", \"process_partner\", \"write_summary\"], \"extra files\": [\"list_partners\", \"process_partner\", \"write_summary\"]}" + }, + { + "name": "uses dynamic task mapping", + "kind": "primary", + "passed": true, + "detail": "mapped tasks: ['process_partner']" + }, + { + "name": "airflow dags test succeeds for 0, 1 and N files", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "one mapped task instance per file (0/1/N)", + "kind": "primary", + "passed": true, + "detail": "expected {'2026-09-01': 0, '2026-09-02': 1, '2026-09-03': 4}; expanded instances per mapped task: {'2026-09-01': {}, '2026-09-02': {'process_partner': 1}, '2026-09-03': {'process_partner': 4}}" + }, + { + "name": "per-partner result files are correct", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "summary is correct, including the day with no files", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun of a day reproduces the same outputs", + "kind": "primary", + "passed": true, + "detail": "2026-09-03: summary expected {'files': 4, 'rows': 30, 'total_amount': 7988.86} got {'files': 4, 'rows': 30, 'total_amount': 7988.86}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "12 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..6f5bf30 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch @@ -0,0 +1,131 @@ +diff --git a/dags/partner_feeds.py b/dags/partner_feeds.py +new file mode 100644 +index 0000000..dd8d29c +--- /dev/null ++++ b/dags/partner_feeds.py +@@ -0,0 +1,125 @@ ++"""Daily ingestion of partner order files. ++ ++For each run date the DAG: ++1. Discovers every .csv in data/partner_drop// ++2. Processes each file as its own dynamically-mapped task instance so ++ a single broken file only fails (and retries) its own work. ++3. Writes output/partner_feeds//.json per file. ++4. Writes output/partner_feeds//summary.json once all per-partner ++ tasks have finished (even on days with zero files). ++ ++Supports catchup from 2026-09-01 so any historical date can be rerun. ++""" ++ ++from __future__ import annotations ++ ++import csv ++import json ++from decimal import ROUND_HALF_UP, Decimal ++from pathlib import Path ++ ++import pendulum ++from airflow.sdk import dag, task ++from airflow.utils.trigger_rule import TriggerRule ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++DROP_ROOT = PROJECT_ROOT / "data" / "partner_drop" ++OUT_ROOT = PROJECT_ROOT / "output" / "partner_feeds" ++ ++ ++@dag( ++ dag_id="partner_feeds", ++ schedule="0 6 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=True, ++ max_active_runs=1, ++ tags=["partners", "feeds"], ++) ++def partner_feeds(): ++ # ------------------------------------------------------------------ ++ # Step 1 – discover files for the run date ++ # ------------------------------------------------------------------ ++ @task ++ def discover_files(ds: str) -> list[dict]: ++ """Return one dict per CSV found; empty list when no folder exists.""" ++ drop_dir = DROP_ROOT / ds ++ if not drop_dir.exists(): ++ return [] ++ return [ ++ {"partner": p.stem, "csv_path": str(p)} ++ for p in sorted(drop_dir.glob("*.csv")) ++ ] ++ ++ # ------------------------------------------------------------------ ++ # Step 2 – process one partner file (dynamically mapped, one per file) ++ # ------------------------------------------------------------------ ++ @task(retries=2, retry_delay=pendulum.duration(seconds=30)) ++ def process_partner_file(partner: str, csv_path: str, ds: str) -> dict: ++ """Read .csv, write .json, return summary row.""" ++ out_dir = OUT_ROOT / ds ++ out_dir.mkdir(parents=True, exist_ok=True) ++ ++ rows = 0 ++ total = Decimal("0") ++ with open(csv_path, newline="") as fh: ++ reader = csv.DictReader(fh) ++ for row in reader: ++ rows += 1 ++ total += Decimal(row["amount"]) ++ ++ total_rounded = float(total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) ++ ++ payload = { ++ "partner": partner, ++ "rows": rows, ++ "total_amount": total_rounded, ++ } ++ out_file = out_dir / f"{partner}.json" ++ out_file.write_text(json.dumps(payload, indent=2)) ++ ++ return payload ++ ++ # ------------------------------------------------------------------ ++ # Step 3 – summarise (runs even if some per-partner tasks failed) ++ # ------------------------------------------------------------------ ++ @task(trigger_rule=TriggerRule.ALL_DONE) ++ def write_summary(results: list, ds: str) -> str: ++ """Aggregate per-partner results and write summary.json. ++ ++ ``results`` is the mapped output list from process_partner_file; ++ individual elements are None when the upstream task failed, so ++ we filter those out rather than crashing the summary. ++ """ ++ out_dir = OUT_ROOT / ds ++ out_dir.mkdir(parents=True, exist_ok=True) ++ ++ valid = [r for r in results if r is not None] ++ ++ total_files = len(valid) ++ total_rows = sum(r["rows"] for r in valid) ++ total_amount = float( ++ Decimal(str(sum(r["total_amount"] for r in valid))).quantize( ++ Decimal("0.01"), rounding=ROUND_HALF_UP ++ ) ++ ) ++ ++ summary = { ++ "files": total_files, ++ "rows": total_rows, ++ "total_amount": total_amount, ++ } ++ out_file = out_dir / "summary.json" ++ out_file.write_text(json.dumps(summary, indent=2)) ++ return str(out_file) ++ ++ # ------------------------------------------------------------------ ++ # Wire up ++ # ------------------------------------------------------------------ ++ files = discover_files() ++ ++ processed = process_partner_file.expand_kwargs(files) ++ ++ write_summary(results=processed) ++ ++ ++partner_feeds() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..c04aec5 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.3333333333333333, + "returncode": 0, + "wall_s": 132.8, + "cost_usd": 1.036033, + "cost_reported_usd": 3.297565, + "tokens": { + "input": 753856, + "output": 5340, + "reasoning": 0, + "cache_read": 542324, + "cache_write": 211520, + "total": 1513040 + }, + "steps": 11, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 3, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0daa0731ffeIFU7UdpWKt15gh.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json new file mode 100644 index 0000000..84e8ee9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-partner-feeds-mapping/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json @@ -0,0 +1,79 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feeds exists", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_feeds', 'partner_registry_export']" + }, + { + "name": "task set is independent of files present at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"fixture data\": [\"discover_files\", \"process_partner_file\", \"write_summary\"], \"no partner_drop dir\": [\"discover_files\", \"process_partner_file\", \"write_summary\"], \"extra files\": [\"discover_files\", \"process_partner_file\", \"write_summary\"]}" + }, + { + "name": "uses dynamic task mapping", + "kind": "primary", + "passed": true, + "detail": "mapped tasks: ['process_partner_file']" + }, + { + "name": "airflow dags test succeeds for 0, 1 and N files", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "one mapped task instance per file (0/1/N)", + "kind": "primary", + "passed": true, + "detail": "expected {'2026-09-01': 0, '2026-09-02': 1, '2026-09-03': 4}; expanded instances per mapped task: {'2026-09-01': {}, '2026-09-02': {'process_partner_file': 1}, '2026-09-03': {'process_partner_file': 4}}" + }, + { + "name": "per-partner result files are correct", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "summary is correct, including the day with no files", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "rerun of a day reproduces the same outputs", + "kind": "primary", + "passed": true, + "detail": "2026-09-03: summary expected {'files': 4, 'rows': 30, 'total_amount': 7988.86} got {'files': 4, 'rows': 30, 'total_amount': 7988.86}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "3 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": false, + "detail": "dags/partner_feeds.py:23 DeprecatedImportWarning: The `airflow.utils.trigger_rule.TriggerRule` attribute is deprecated. Please use `'airflow.task.trigger_rule.TriggerRule" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.3333333333333333 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..9cf03fc --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,110 @@ +diff --git a/dags/vendor_shipments.py b/dags/vendor_shipments.py +new file mode 100644 +index 0000000..f799682 +--- /dev/null ++++ b/dags/vendor_shipments.py +@@ -0,0 +1,72 @@ ++"""Load the daily vendor shipments file and publish the carrier report.""" ++ ++from __future__ import annotations ++ ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.models import Variable ++from airflow.providers.standard.operators.bash import BashOperator ++from airflow.providers.standard.sensors.filesystem import FileSensor ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" ++ ++ ++@dag( ++ schedule="0 6 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, ++ tags=["shipments", "warehouse"], ++) ++def vendor_shipments(): ++ wait_for_file = FileSensor( ++ task_id="wait_for_file", ++ filepath="{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv", ++ poke_interval=300, # Check every 5 minutes ++ timeout=64800, # 18 hours (6am + 18h = midnight, so it fails if not there by end of day) ++ mode="reschedule", # Don't hold a worker slot while waiting ++ ) ++ ++ @task ++ def load_shipments(ds=None): ++ import duckdb ++ ++ WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) ++ ++ # Get the landing directory from Airflow Variable ++ landing_dir = Variable.get("vendor_landing_dir", default_var=str(PROJECT_ROOT / "data" / "incoming")) ++ shipments_file = Path(landing_dir) / f"shipments_{ds}.csv" ++ ++ with duckdb.connect(str(WAREHOUSE)) as con: ++ con.execute( ++ "CREATE TABLE IF NOT EXISTS vendor_shipments (" ++ "shipment_id VARCHAR, carrier VARCHAR, destination VARCHAR, " ++ "weight_kg DOUBLE, shipped_at TIMESTAMP, ship_date DATE" ++ ")" ++ ) ++ # Replace the day's rows so a rerun never duplicates them. ++ con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) ++ con.execute( ++ "INSERT INTO vendor_shipments " ++ "SELECT shipment_id, carrier, destination, weight_kg, shipped_at, CAST(? AS DATE) as ship_date " ++ "FROM read_csv_auto(?)", ++ [ds, str(shipments_file)], ++ ) ++ return con.execute( ++ "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] ++ ).fetchone()[0] ++ ++ publish_report = BashOperator( ++ task_id="publish_report", ++ bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", ++ ) ++ ++ # Orchestration: wait for file, load it, then publish the report ++ wait_for_file >> load_shipments() >> publish_report ++ ++ ++vendor_shipments() +diff --git a/data/incoming/shipments_2026-09-30.csv b/data/incoming/shipments_2026-09-30.csv +new file mode 100644 +index 0000000..82e9921 +--- /dev/null ++++ b/data/incoming/shipments_2026-09-30.csv +@@ -0,0 +1,26 @@ ++shipment_id,carrier,destination,weight_kg,shipped_at ++SHP-20260928-0000,DHL,Chicago,4.89,2026-09-28 04:36:00 ++SHP-20260928-0001,USPS,Boston,3.94,2026-09-28 14:30:00 ++SHP-20260928-0002,USPS,Denver,24.38,2026-09-28 00:57:00 ++SHP-20260928-0003,USPS,Chicago,28.92,2026-09-28 00:44:00 ++SHP-20260928-0004,DHL,Chicago,1.42,2026-09-28 07:37:00 ++SHP-20260928-0005,DHL,Denver,27.52,2026-09-28 00:41:00 ++SHP-20260928-0006,DHL,Seattle,9.02,2026-09-28 13:46:00 ++SHP-20260928-0007,FedEx,Chicago,9.39,2026-09-28 14:31:00 ++SHP-20260928-0008,USPS,Chicago,37.07,2026-09-28 07:48:00 ++SHP-20260928-0009,DHL,Boston,25.25,2026-09-28 13:53:00 ++SHP-20260928-0010,UPS,Austin,29.78,2026-09-28 23:55:00 ++SHP-20260928-0011,USPS,Seattle,33.24,2026-09-28 23:45:00 ++SHP-20260928-0012,UPS,Chicago,23.59,2026-09-28 21:12:00 ++SHP-20260928-0013,USPS,Seattle,34.16,2026-09-28 15:54:00 ++SHP-20260928-0014,USPS,Denver,26.66,2026-09-28 15:15:00 ++SHP-20260928-0015,UPS,Austin,17.67,2026-09-28 11:35:00 ++SHP-20260928-0016,FedEx,Seattle,33.63,2026-09-28 16:06:00 ++SHP-20260928-0017,DHL,Denver,1.93,2026-09-28 11:31:00 ++SHP-20260928-0018,USPS,Boston,6.91,2026-09-28 22:54:00 ++SHP-20260928-0019,FedEx,Seattle,36.82,2026-09-28 07:00:00 ++SHP-20260928-0020,USPS,Seattle,13.88,2026-09-28 17:14:00 ++SHP-20260928-0021,USPS,Chicago,26.44,2026-09-28 18:22:00 ++SHP-20260928-0022,DHL,Denver,31.39,2026-09-28 19:46:00 ++SHP-20260928-0023,FedEx,Seattle,31.14,2026-09-28 23:32:00 ++SHP-20260928-0024,DHL,Denver,34.83,2026-09-28 06:27:00 diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..600921c --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 266.1, + "cost_usd": 0.700714, + "cost_reported_usd": 3.2588, + "tokens": { + "input": 2558286, + "output": 17984, + "reasoning": 0, + "cache_read": 2249577, + "cache_write": 308509, + "total": 5134356 + }, + "steps": 37, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 24, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd064acffe0h7WXKewiO8lhK.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..be1311b --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,97 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly with no Variables defined", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG vendor_shipments exists", + "kind": "primary", + "passed": true, + "detail": "found: ['vendor_shipments', 'warehouse_orders']" + }, + { + "name": "no Variable/Connection/metadata-DB access at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"calls\": [], \"dags\": [\"vendor_shipments\", \"warehouse_orders\"], \"errors\": {}}" + }, + { + "name": "task graph is wait-for-file -> load -> bash report", + "kind": "primary", + "passed": true, + "detail": "sensor(s) ['wait_for_file'] -> ['load_shipments'] -> publish_report" + }, + { + "name": "file sensor is reschedule-mode or deferrable", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": false, \"timeout_s\": 64800.0}]" + }, + { + "name": "file sensor has an explicit timeout of at most 24h", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": false, \"timeout_s\": 64800.0}]" + }, + { + "name": "runs daily at 06:00 UTC with catchup disabled", + "kind": "primary", + "passed": true, + "detail": "catchup=False runs=[{'start': '2026-10-01T06:00:00+00:00', 'end': '2026-10-01T06:00:00+00:00', 'run_after': '2026-10-01T06:00:00+00:00'}, {'start': '2026-10-02T06:00:00+00:00', 'end': '2026-10-02T06:00:00+00:00', 'run_after': '2026-10-02T06:00:00+00:00'}, {'start': '2026-10-03T06:00:00+00:00', 'end': '2026-10-03T06:00:00+00:00', 'run_after': '2026-10-03T06:00:00+00:00'}]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "vendor_shipments holds exactly the landing file rows per ship_date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': 'expected 42 rows, got 42', '2026-09-28': 'expected 31 rows, got 31'}" + }, + { + "name": "carrier report published for each run date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': {'expected': {'UPS': (11, 185.74), 'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'USPS': (10, 183.08)}, 'got': {'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'UPS': (11, 185.74), 'USPS': (10, 183.08)}}, '2026-09-28': {'expected': {'USPS': (6, 146.97), 'FedEx': (11, 155.6), 'DHL': (9, 185.47), 'UPS': (5, 75.3)}, 'got': {'DHL': (9, 185.47), 'FedEx': (11, 155.6), 'UPS': (5, 75.3), 'USPS': (6, 146.97)}}}" + }, + { + "name": "rerun of a day does not duplicate rows and keeps other days", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; {'2026-09-29': 42, '2026-09-28': 31}" + }, + { + "name": "load waits for the day's file (nothing runs when the file is missing)", + "kind": "primary", + "passed": true, + "detail": "no shipments_2026-09-27.csv in the landing dir; after 150s (timed_out=True) task states: {'wait_for_file': 'up_for_reschedule', 'publish_report': 'None', 'load_shipments': 'None'}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": false, + "detail": "dags/vendor_shipments.py:41 AIR311" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..7054d81 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch @@ -0,0 +1,242 @@ +diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md +new file mode 100644 +index 0000000..5180ff8 +--- /dev/null ++++ b/DEPLOYMENT.md +@@ -0,0 +1,144 @@ ++# Deployment Guide: vendor_shipments DAG ++ ++## Summary ++A new Airflow DAG `vendor_shipments` has been created to: ++1. Wait for daily carrier shipment CSV files ++2. Load them into a DuckDB warehouse table ++3. Publish carrier reports ++ ++## Files Created ++- `dags/vendor_shipments.py` - The main DAG definition ++ ++## Deployment Steps ++ ++### 1. Set the Airflow Variable ++Before deploying the DAG, configure the landing directory path for your environment: ++ ++```bash ++# Development/Staging ++airflow variables set vendor_landing_dir /path/to/dev/shipments/landing ++ ++# Production ++airflow variables set vendor_landing_dir /path/to/prod/shipments/landing ++``` ++ ++This variable will be different for each environment where the aggregator drops files. ++ ++### 2. Deploy the DAG ++Copy the DAG file to your Airflow DAGs directory: ++ ++```bash ++# The DAG is already at: dags/vendor_shipments.py ++# Airflow will auto-discover it on the next scan ++``` ++ ++### 3. Verify the DAG is Loaded ++```bash ++airflow dags list | grep vendor_shipments ++``` ++ ++You should see: ++``` ++vendor_shipments | Load the daily vendor shipments file and publish the carrier report | 1 | 0 | 2026-09-01T00:00:00+00:00 | day | UTC ++``` ++ ++## Task Breakdown ++ ++### wait_for_shipments_file (FileSensor) ++- **Purpose**: Polls for the day's CSV file in the landing directory ++- **Timeout**: 20 hours (from 6 AM until midnight) ++- **Check Frequency**: Every 60 seconds ++- **Failure**: Will fail if file not found within timeout, triggering on-call alert ++- **Worker Impact**: Minimal (poke mode polling) ++ ++### load_vendor_shipments (@task) ++- **Purpose**: Load CSV into DuckDB warehouse ++- **Inputs**: CSV file path, run date ++- **Outputs**: Row count loaded ++- **Deduplication**: Deletes existing rows for the date before inserting ++- **Table**: `vendor_shipments` (auto-created with inferred schema) ++- **Schema**: All CSV columns + `ship_date` column ++ ++### publish_shipments_report (BashOperator) ++- **Purpose**: Generate and publish the carrier report ++- **Script**: `scripts/publish_shipments_report.sh` ++- **Input**: Run date (passed as argument) ++- **Output**: `reports/shipments_YYYY-MM-DD.csv` ++ ++## Running the DAG ++ ++### Automatic Schedule ++The DAG runs automatically every day at **06:00 UTC**. ++ ++### Manual Trigger (for past dates) ++To rerun a specific date (e.g., 2026-09-15): ++ ++```bash ++# Via CLI ++airflow dags backfill vendor_shipments --start-date 2026-09-15 --end-date 2026-09-15 ++ ++# Or trigger via Web UI: ++# 1. Navigate to the DAG ++# 2. Click "Trigger DAG" ++# 3. Set the execution date ++# 4. Run ++``` ++ ++### Rerun Safety ++The DAG is fully idempotent: ++- If you rerun the same date, it deletes existing rows for that date before inserting ++- No duplicate data will be created ++- Safe to rerun any day since 2026-09-01 ++ ++## Monitoring ++ ++### Expected Behavior ++1. **06:00 UTC**: wait_for_shipments_file task starts ++2. **Before 09:00 UTC** (usually): File arrives and sensor detects it ++3. **Immediately after**: load_vendor_shipments runs ++4. **Within seconds**: publish_shipments_report runs ++5. **Total time**: Usually completes within 30 minutes ++ ++### What to Watch ++- Check the web UI under "vendor_shipments" DAG ++- Look for task status changes in the graph view ++- Verify the reports directory for `shipments_*.csv` output files ++ ++### If Something Fails ++ ++**File doesn't arrive by timeout:** ++- Check that `vendor_landing_dir` variable is set correctly ++- Verify the aggregator is running and dropping files ++- Confirm the filename format: `shipments_YYYY-MM-DD.csv` ++ ++**Load task fails:** ++- Check warehouse connectivity to DuckDB ++- Verify the CSV file format hasn't changed ++- Manual retry is automatic (2 retries configured) ++ ++**Publish task fails:** ++- Check that `scripts/publish_shipments_report.sh` is executable ++- Verify the `reports/` directory exists ++- Ensure Python dependencies are installed (`scripts/build_shipments_report.py` requirements) ++ ++## Conventions Followed ++- Same patterns as existing `warehouse_orders` DAG ++- Uses Airflow SDK decorators (`@dag`, `@task`) ++- Default owner: logistics-analytics ++- Default retries: 2 with 5-minute delay between retries ++- UTC timezone throughout ++- No automatic backfilling on deploy (catchup=False) ++ ++## Rollback ++To disable the DAG temporarily: ++```bash ++airflow dags pause vendor_shipments ++airflow dags unpause vendor_shipments ++``` ++ ++To remove the DAG entirely: ++```bash ++# Delete the file ++rm dags/vendor_shipments.py ++# Airflow will auto-discover the deletion on next scan ++``` +diff --git a/dags/vendor_shipments.py b/dags/vendor_shipments.py +new file mode 100644 +index 0000000..16df06f +--- /dev/null ++++ b/dags/vendor_shipments.py +@@ -0,0 +1,86 @@ ++"""Load the daily vendor shipments file and publish the carrier report.""" ++ ++from __future__ import annotations ++ ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.models import Variable ++from airflow.providers.standard.operators.bash import BashOperator ++from airflow.providers.standard.sensors.filesystem import FileSensor ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" ++ ++ ++@dag( ++ schedule="0 6 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, ++ tags=["vendor", "shipments", "warehouse"], ++) ++def vendor_shipments(): ++ """ ++ Load the daily vendor shipments file from the landing directory into the warehouse. ++ ++ The CSV file is expected to be named shipments_YYYY-MM-DD.csv and arrive in the ++ directory specified by the Airflow Variable 'vendor_landing_dir'. If the file is ++ not present by end of day, the task will fail and alert on-call. ++ """ ++ ++ # Wait for the shipments file. Use a sensor that doesn't tie up a worker slot. ++ # The file usually arrives before 9am UTC. If not present by end of day (23:59), ++ # poke_interval and timeout will cause failure. ++ # Use Jinja templating to defer Variable.get() to runtime. ++ wait_for_shipments_file = FileSensor( ++ task_id="wait_for_shipments_file", ++ filepath="{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv", ++ poke_interval=60, # Check every minute ++ timeout=72000, # 20 hours, to cover from 6am to midnight UTC ++ mode="poke", # Default mode; worker will poll ++ ) ++ ++ @task ++ def load_vendor_shipments(ds=None): ++ """Load the vendor shipments CSV into the warehouse, deduplicating on rerun.""" ++ import duckdb ++ ++ landing_dir = Variable.get("vendor_landing_dir") ++ shipments_file = Path(landing_dir) / f"shipments_{ds}.csv" ++ ++ WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) ++ with duckdb.connect(str(WAREHOUSE)) as con: ++ # Create table if it doesn't exist. Infer schema from CSV. ++ con.execute( ++ "CREATE TABLE IF NOT EXISTS vendor_shipments AS " ++ "SELECT *, CAST(? AS DATE) AS ship_date " ++ "FROM read_csv_auto(?) " ++ "WHERE FALSE", ++ [ds, str(shipments_file)], ++ ) ++ # Delete any existing rows for this date to handle reruns. ++ con.execute("DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds]) ++ # Insert today's rows with the ship_date column. ++ con.execute( ++ "INSERT INTO vendor_shipments " ++ "SELECT *, CAST(? AS DATE) AS ship_date " ++ "FROM read_csv_auto(?)", ++ [ds, str(shipments_file)], ++ ) ++ row_count = con.execute( ++ "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", [ds] ++ ).fetchone()[0] ++ return row_count ++ ++ publish_shipments_report = BashOperator( ++ task_id="publish_shipments_report", ++ bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", ++ ) ++ ++ wait_for_shipments_file >> load_vendor_shipments() >> publish_shipments_report ++ ++ ++vendor_shipments() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..08126c1 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "task_fail", + "reason": "failed primary: file sensor is reschedule-mode or deferrable", + "primary_pass": false, + "primary_score": 0.9166666666666666, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 165.0, + "cost_usd": 0.477223, + "cost_reported_usd": 2.319464, + "tokens": { + "input": 1842387, + "output": 9196, + "reasoning": 0, + "cache_read": 1627569, + "cache_write": 214672, + "total": 3693824 + }, + "steps": 26, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 17, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0da78be3ffe6FhgDBoZNprySk.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json new file mode 100644 index 0000000..bdca133 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json @@ -0,0 +1,97 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly with no Variables defined", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG vendor_shipments exists", + "kind": "primary", + "passed": true, + "detail": "found: ['vendor_shipments', 'warehouse_orders']" + }, + { + "name": "no Variable/Connection/metadata-DB access at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"calls\": [], \"dags\": [\"vendor_shipments\", \"warehouse_orders\"], \"errors\": {}}" + }, + { + "name": "task graph is wait-for-file -> load -> bash report", + "kind": "primary", + "passed": true, + "detail": "sensor(s) ['wait_for_shipments_file'] -> ['load_vendor_shipments'] -> publish_shipments_report" + }, + { + "name": "file sensor is reschedule-mode or deferrable", + "kind": "primary", + "passed": false, + "detail": "[{\"task_id\": \"wait_for_shipments_file\", \"class\": \"FileSensor\", \"mode\": \"poke\", \"deferrable\": false, \"timeout_s\": 72000.0}]" + }, + { + "name": "file sensor has an explicit timeout of at most 24h", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_shipments_file\", \"class\": \"FileSensor\", \"mode\": \"poke\", \"deferrable\": false, \"timeout_s\": 72000.0}]" + }, + { + "name": "runs daily at 06:00 UTC with catchup disabled", + "kind": "primary", + "passed": true, + "detail": "catchup=False runs=[{'start': '2026-10-01T06:00:00+00:00', 'end': '2026-10-01T06:00:00+00:00', 'run_after': '2026-10-01T06:00:00+00:00'}, {'start': '2026-10-02T06:00:00+00:00', 'end': '2026-10-02T06:00:00+00:00', 'run_after': '2026-10-02T06:00:00+00:00'}, {'start': '2026-10-03T06:00:00+00:00', 'end': '2026-10-03T06:00:00+00:00', 'run_after': '2026-10-03T06:00:00+00:00'}]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "vendor_shipments holds exactly the landing file rows per ship_date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': 'expected 42 rows, got 42', '2026-09-28': 'expected 31 rows, got 31'}" + }, + { + "name": "carrier report published for each run date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': {'expected': {'UPS': (11, 185.74), 'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'USPS': (10, 183.08)}, 'got': {'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'UPS': (11, 185.74), 'USPS': (10, 183.08)}}, '2026-09-28': {'expected': {'USPS': (6, 146.97), 'FedEx': (11, 155.6), 'DHL': (9, 185.47), 'UPS': (5, 75.3)}, 'got': {'DHL': (9, 185.47), 'FedEx': (11, 155.6), 'UPS': (5, 75.3), 'USPS': (6, 146.97)}}}" + }, + { + "name": "rerun of a day does not duplicate rows and keeps other days", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; {'2026-09-29': 42, '2026-09-28': 31}" + }, + { + "name": "load waits for the day's file (nothing runs when the file is missing)", + "kind": "primary", + "passed": true, + "detail": "no shipments_2026-09-27.csv in the landing dir; after 150s (timed_out=True) task states: {'wait_for_shipments_file': 'running', 'publish_shipments_report': 'None', 'load_vendor_shipments': 'None'}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bairflow\\\\s+dags\\\\s+(test|list-import-errors|list|reserialize)\\\\b']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": false, + "detail": "dags/vendor_shipments.py:51 AIR311" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.9166666666666666, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..67eba10 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,130 @@ +diff --git a/dags/vendor_shipments.py b/dags/vendor_shipments.py +new file mode 100644 +index 0000000..59abb5c +--- /dev/null ++++ b/dags/vendor_shipments.py +@@ -0,0 +1,124 @@ ++"""Wait for the daily vendor shipments file, load it into DuckDB, and publish the carrier report.""" ++ ++from __future__ import annotations ++ ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.models import Variable ++from airflow.providers.standard.operators.bash import BashOperator ++from airflow.providers.standard.sensors.filesystem import FileSensor ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" ++ ++# The file usually arrives before 09:00 UTC but may be late. ++# The DAG starts at 06:00 UTC; we allow up to 23:59 UTC (17 h 59 m = 64 740 s). ++_SENSOR_TIMEOUT_SECONDS = int(timedelta(hours=17, minutes=59).total_seconds()) # 64 740 ++_SENSOR_POKE_INTERVAL_SECONDS = 5 * 60 # 5 minutes – keeps worker slots free between polls ++ ++ ++@dag( ++ schedule="0 6 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={ ++ "owner": "logistics-analytics", ++ "retries": 2, ++ "retry_delay": timedelta(minutes=5), ++ }, ++ tags=["shipments", "warehouse"], ++) ++def vendor_shipments(): ++ # ------------------------------------------------------------------------- ++ # 1. Wait for the day's file without occupying a worker slot. ++ # ++ # FileSensor with deferrable=True hands control to the Triggerer process ++ # while it polls, freeing the worker slot for other tasks. ++ # ++ # filepath is a Jinja template: at run-time Airflow resolves ++ # {{ var.value.vendor_landing_dir }} → the Airflow Variable ++ # {{ ds }} → the logical date (YYYY-MM-DD) ++ # ++ # The fs_default connection must have an empty base path (the default) so ++ # that the absolute path produced by the template is used unchanged. ++ # ------------------------------------------------------------------------- ++ wait_for_file = FileSensor( ++ task_id="wait_for_file", ++ filepath="{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv", ++ fs_conn_id="fs_default", ++ poke_interval=_SENSOR_POKE_INTERVAL_SECONDS, ++ timeout=_SENSOR_TIMEOUT_SECONDS, ++ mode="reschedule", # belt-and-suspenders: free worker slot even if deferrable is off ++ deferrable=True, # primary mechanism: use Triggerer, not a worker slot ++ soft_fail=False, # hard-fail so on-call gets paged when the file is missing ++ ) ++ ++ # ------------------------------------------------------------------------- ++ # 2. Load the file into the vendor_shipments table. ++ # ++ # Idempotent: DELETE the day's rows before re-inserting so reruns never ++ # produce duplicates. The table is created on first use. ++ # The file's columns (shipment_id, carrier, destination, weight_kg, ++ # shipped_at) are loaded as-is; ship_date is added from the logical date. ++ # ------------------------------------------------------------------------- ++ @task ++ def load_shipments(ds=None): ++ import duckdb ++ ++ landing_dir = Variable.get("vendor_landing_dir") ++ csv_path = f"{landing_dir}/shipments_{ds}.csv" ++ ++ WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) ++ with duckdb.connect(str(WAREHOUSE)) as con: ++ con.execute( ++ """ ++ CREATE TABLE IF NOT EXISTS vendor_shipments ( ++ shipment_id VARCHAR, ++ carrier VARCHAR, ++ destination VARCHAR, ++ weight_kg DOUBLE, ++ shipped_at TIMESTAMP, ++ ship_date DATE ++ ) ++ """ ++ ) ++ # Remove any existing rows for this date so reruns are safe. ++ con.execute( ++ "DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", ++ [ds], ++ ) ++ con.execute( ++ """ ++ INSERT INTO vendor_shipments ++ SELECT ++ shipment_id, ++ carrier, ++ destination, ++ CAST(weight_kg AS DOUBLE), ++ CAST(shipped_at AS TIMESTAMP), ++ CAST(? AS DATE) AS ship_date ++ FROM read_csv_auto(?) ++ """, ++ [ds, csv_path], ++ ) ++ row_count = con.execute( ++ "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", ++ [ds], ++ ).fetchone()[0] ++ return row_count ++ ++ # ------------------------------------------------------------------------- ++ # 3. Publish the carrier report for the loaded date. ++ # ------------------------------------------------------------------------- ++ publish_report = BashOperator( ++ task_id="publish_report", ++ bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", ++ ) ++ ++ wait_for_file >> load_shipments() >> publish_report ++ ++ ++vendor_shipments() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..6cd655e --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,38 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 245.5, + "cost_usd": 1.811411, + "cost_reported_usd": 8.585213, + "tokens": { + "input": 2257966, + "output": 8248, + "reasoning": 0, + "cache_read": 1965118, + "cache_write": 292816, + "total": 4524148 + }, + "steps": 31, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [ + { + "name": "creating-dbt-models", + "origin": "global", + "status": "completed" + } + ], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 21, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dd1cc26ffe8bHWSJ4q3JvFSw.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..d547a1f --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,97 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly with no Variables defined", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG vendor_shipments exists", + "kind": "primary", + "passed": true, + "detail": "found: ['vendor_shipments', 'warehouse_orders']" + }, + { + "name": "no Variable/Connection/metadata-DB access at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"calls\": [], \"dags\": [\"vendor_shipments\", \"warehouse_orders\"], \"errors\": {}}" + }, + { + "name": "task graph is wait-for-file -> load -> bash report", + "kind": "primary", + "passed": true, + "detail": "sensor(s) ['wait_for_file'] -> ['load_shipments'] -> publish_report" + }, + { + "name": "file sensor is reschedule-mode or deferrable", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": true, \"timeout_s\": 64740.0}]" + }, + { + "name": "file sensor has an explicit timeout of at most 24h", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": true, \"timeout_s\": 64740.0}]" + }, + { + "name": "runs daily at 06:00 UTC with catchup disabled", + "kind": "primary", + "passed": true, + "detail": "catchup=False runs=[{'start': '2026-10-01T06:00:00+00:00', 'end': '2026-10-01T06:00:00+00:00', 'run_after': '2026-10-01T06:00:00+00:00'}, {'start': '2026-10-02T06:00:00+00:00', 'end': '2026-10-02T06:00:00+00:00', 'run_after': '2026-10-02T06:00:00+00:00'}, {'start': '2026-10-03T06:00:00+00:00', 'end': '2026-10-03T06:00:00+00:00', 'run_after': '2026-10-03T06:00:00+00:00'}]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "vendor_shipments holds exactly the landing file rows per ship_date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': 'expected 42 rows, got 42', '2026-09-28': 'expected 31 rows, got 31'}" + }, + { + "name": "carrier report published for each run date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': {'expected': {'UPS': (11, 185.74), 'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'USPS': (10, 183.08)}, 'got': {'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'UPS': (11, 185.74), 'USPS': (10, 183.08)}}, '2026-09-28': {'expected': {'USPS': (6, 146.97), 'FedEx': (11, 155.6), 'DHL': (9, 185.47), 'UPS': (5, 75.3)}, 'got': {'DHL': (9, 185.47), 'FedEx': (11, 155.6), 'UPS': (5, 75.3), 'USPS': (6, 146.97)}}}" + }, + { + "name": "rerun of a day does not duplicate rows and keeps other days", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; {'2026-09-29': 42, '2026-09-28': 31}" + }, + { + "name": "load waits for the day's file (nothing runs when the file is missing)", + "kind": "primary", + "passed": true, + "detail": "no shipments_2026-09-27.csv in the landing dir; after 150s (timed_out=True) task states: {'wait_for_file': 'queued', 'publish_report': 'None', 'load_shipments': 'None'}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": true, + "detail": "matched: ['\\\\bDagBag\\\\b', '\\\\bpython[0-9.]*\\\\s+\\\\S*dags/\\\\S+\\\\.py']" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": false, + "detail": "dags/vendor_shipments.py:71 AIR311" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..12f4a93 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/agent.patch @@ -0,0 +1,111 @@ +diff --git a/dags/vendor_shipments.py b/dags/vendor_shipments.py +new file mode 100644 +index 0000000..237d848 +--- /dev/null ++++ b/dags/vendor_shipments.py +@@ -0,0 +1,105 @@ ++"""Wait for the daily carrier-aggregator file, load it into DuckDB, and publish the report.""" ++ ++from __future__ import annotations ++ ++from datetime import timedelta ++from pathlib import Path ++ ++import pendulum ++from airflow.models import Variable ++from airflow.providers.standard.operators.bash import BashOperator ++from airflow.providers.standard.sensors.filesystem import FileSensor ++from airflow.sdk import dag, task ++ ++PROJECT_ROOT = Path(__file__).resolve().parents[1] ++WAREHOUSE = PROJECT_ROOT / "warehouse" / "analytics.duckdb" ++ ++ ++@dag( ++ schedule="0 6 * * *", ++ start_date=pendulum.datetime(2026, 9, 1, tz="UTC"), ++ catchup=False, ++ default_args={"owner": "logistics-analytics", "retries": 2, "retry_delay": timedelta(minutes=5)}, ++ tags=["shipments", "warehouse"], ++) ++def vendor_shipments(): ++ # ------------------------------------------------------------------ # ++ # 1. Wait for the file without occupying a worker slot. # ++ # The file is named shipments_YYYY-MM-DD.csv; ds holds YYYY-MM-DD. # ++ # timeout = 18 h: from 06:00 UTC until midnight of the same day. # ++ # mode="reschedule" releases the worker slot between polls. # ++ # ------------------------------------------------------------------ # ++ wait_for_file = FileSensor( ++ task_id="wait_for_file", ++ filepath="{{ var.value.vendor_landing_dir }}/shipments_{{ ds }}.csv", ++ mode="reschedule", ++ poke_interval=300, # check every 5 minutes ++ timeout=18 * 3600, # fail after 18 hours (end of day) ++ soft_fail=False, # a timeout → task failure → on-call page ++ ) ++ ++ # ------------------------------------------------------------------ # ++ # 2. Load the file into the vendor_shipments table. # ++ # Idempotent: delete the day's rows first so reruns never duplicate. # ++ # Adds ship_date (DATE) derived from the filename / ds context var. # ++ # ------------------------------------------------------------------ # ++ @task ++ def load_shipments(ds=None): ++ import duckdb ++ ++ landing_dir = Variable.get("vendor_landing_dir") ++ csv_path = Path(landing_dir) / f"shipments_{ds}.csv" ++ ++ WAREHOUSE.parent.mkdir(parents=True, exist_ok=True) ++ with duckdb.connect(str(WAREHOUSE)) as con: ++ con.execute( ++ """ ++ CREATE TABLE IF NOT EXISTS vendor_shipments ( ++ shipment_id VARCHAR, ++ carrier VARCHAR, ++ destination VARCHAR, ++ weight_kg DOUBLE, ++ shipped_at TIMESTAMP, ++ ship_date DATE ++ ) ++ """ ++ ) ++ # Remove any existing rows for this day so reruns are idempotent. ++ con.execute( ++ "DELETE FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", ++ [ds], ++ ) ++ # Load all columns from the CSV and append ship_date. ++ con.execute( ++ """ ++ INSERT INTO vendor_shipments ++ SELECT ++ shipment_id, ++ carrier, ++ destination, ++ CAST(weight_kg AS DOUBLE), ++ CAST(shipped_at AS TIMESTAMP), ++ CAST(? AS DATE) AS ship_date ++ FROM read_csv_auto(?) ++ """, ++ [ds, str(csv_path)], ++ ) ++ loaded = con.execute( ++ "SELECT count(*) FROM vendor_shipments WHERE ship_date = CAST(? AS DATE)", ++ [ds], ++ ).fetchone()[0] ++ print(f"Loaded {loaded} rows for ship_date={ds}") ++ return loaded ++ ++ # ------------------------------------------------------------------ # ++ # 3. Publish the carrier report for the day. # ++ # ------------------------------------------------------------------ # ++ publish_report = BashOperator( ++ task_id="publish_report", ++ bash_command=f"{PROJECT_ROOT}/scripts/publish_shipments_report.sh {{{{ ds }}}}", ++ ) ++ ++ wait_for_file >> load_shipments() >> publish_report ++ ++ ++vendor_shipments() diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..61d5ad7 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/attempt.json @@ -0,0 +1,38 @@ +{ + "status": "ok", + "reason": "primary checks passed", + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.3333333333333333, + "returncode": 0, + "wall_s": 122.8, + "cost_usd": 1.050531, + "cost_reported_usd": 3.534747, + "tokens": { + "input": 828085, + "output": 4897, + "reasoning": 0, + "cache_read": 616879, + "cache_write": 211193, + "total": 1661054 + }, + "steps": 12, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [ + { + "name": "creating-dbt-models", + "origin": "global", + "status": "completed" + } + ], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 3, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0da8a020ffeOvml6QjRtQL45j.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json new file mode 100644 index 0000000..e27087e --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/authoring-vendor-shipments-etl/google-vertex-anthropic_claude-sonnet-4-6_default/run-2/attempt-0/grade.json @@ -0,0 +1,97 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly with no Variables defined", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG vendor_shipments exists", + "kind": "primary", + "passed": true, + "detail": "found: ['vendor_shipments', 'warehouse_orders']" + }, + { + "name": "no Variable/Connection/metadata-DB access at parse time", + "kind": "primary", + "passed": true, + "detail": "{\"calls\": [], \"dags\": [\"vendor_shipments\", \"warehouse_orders\"], \"errors\": {}}" + }, + { + "name": "task graph is wait-for-file -> load -> bash report", + "kind": "primary", + "passed": true, + "detail": "sensor(s) ['wait_for_file'] -> ['load_shipments'] -> publish_report" + }, + { + "name": "file sensor is reschedule-mode or deferrable", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": false, \"timeout_s\": 64800.0}]" + }, + { + "name": "file sensor has an explicit timeout of at most 24h", + "kind": "primary", + "passed": true, + "detail": "[{\"task_id\": \"wait_for_file\", \"class\": \"FileSensor\", \"mode\": \"reschedule\", \"deferrable\": false, \"timeout_s\": 64800.0}]" + }, + { + "name": "runs daily at 06:00 UTC with catchup disabled", + "kind": "primary", + "passed": true, + "detail": "catchup=False runs=[{'start': '2026-10-01T06:00:00+00:00', 'end': '2026-10-01T06:00:00+00:00', 'run_after': '2026-10-01T06:00:00+00:00'}, {'start': '2026-10-02T06:00:00+00:00', 'end': '2026-10-02T06:00:00+00:00', 'run_after': '2026-10-02T06:00:00+00:00'}, {'start': '2026-10-03T06:00:00+00:00', 'end': '2026-10-03T06:00:00+00:00', 'run_after': '2026-10-03T06:00:00+00:00'}]" + }, + { + "name": "airflow dags test succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "vendor_shipments holds exactly the landing file rows per ship_date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': 'expected 42 rows, got 42', '2026-09-28': 'expected 31 rows, got 31'}" + }, + { + "name": "carrier report published for each run date", + "kind": "primary", + "passed": true, + "detail": "{'2026-09-29': {'expected': {'UPS': (11, 185.74), 'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'USPS': (10, 183.08)}, 'got': {'DHL': (11, 260.51), 'FedEx': (10, 293.5), 'UPS': (11, 185.74), 'USPS': (10, 183.08)}}, '2026-09-28': {'expected': {'USPS': (6, 146.97), 'FedEx': (11, 155.6), 'DHL': (9, 185.47), 'UPS': (5, 75.3)}, 'got': {'DHL': (9, 185.47), 'FedEx': (11, 155.6), 'UPS': (5, 75.3), 'USPS': (6, 146.97)}}}" + }, + { + "name": "rerun of a day does not duplicate rows and keeps other days", + "kind": "primary", + "passed": true, + "detail": "second run ok=True; {'2026-09-29': 42, '2026-09-28': 31}" + }, + { + "name": "load waits for the day's file (nothing runs when the file is missing)", + "kind": "primary", + "passed": true, + "detail": "no shipments_2026-09-27.csv in the landing dir; after 150s (timed_out=True) task states: {'wait_for_file': 'up_for_reschedule', 'publish_report': 'None', 'load_shipments': 'None'}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "3 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": false, + "detail": "dags/vendor_shipments.py:50 AIR311" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": true, + "primary_score": 1.0, + "secondary_score": 0.3333333333333333 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..98aca48 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/agent.patch @@ -0,0 +1,22 @@ +diff --git a/dags/partner_feed.py b/dags/partner_feed.py +index fffc488..ab720d3 100644 +--- a/dags/partner_feed.py ++++ b/dags/partner_feed.py +@@ -30,7 +30,7 @@ FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + def partner_feed(): + @task + def extract_orders(**context) -> str: +- day = context["logical_date"].strftime("%Y-%m-%d") ++ day = context["data_interval_start"].strftime("%Y-%m-%d") + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} +@@ -47,7 +47,7 @@ def partner_feed(): + + @task + def write_manifest(feed_path: str, **context) -> str: +- day = context["execution_date"].strftime("%Y-%m-%d") ++ day = context["data_interval_start"].strftime("%Y-%m-%d") + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..1509989 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "task_fail", + "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", + "primary_pass": false, + "primary_score": 0.7142857142857143, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 84.5, + "cost_usd": 0.332797, + "cost_reported_usd": 1.138662, + "tokens": { + "input": 805929, + "output": 4215, + "reasoning": 0, + "cache_read": 604933, + "cache_write": 200932, + "total": 1616009 + }, + "steps": 12, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 7, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcd84d0ffeUr3arKaVLLAd0D.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json new file mode 100644 index 0000000..0e7cc78 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feed is present", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_catalog_sync', 'partner_feed']" + }, + { + "name": "task graph preserved (extract -> manifest -> deliver)", + "kind": "primary", + "passed": true, + "detail": "missing=[] broken=[] deps=[['extract_orders', 'write_manifest'], ['write_manifest', 'deliver']]" + }, + { + "name": "dags test with logical date 2026-03-02 succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "dated run delivers the 2026-03-02 feed", + "kind": "primary", + "passed": true, + "detail": "ok" + }, + { + "name": "manual run without a logical date succeeds", + "kind": "primary", + "passed": false, + "detail": "result={'state': 'failed', 'logical_date': None, 'run_after': '2026-03-03T00:30:00+00:00'} non-success tasks=['deliver', 'extract_orders', 'write_manifest']\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 2234, in _execute_task\n result = _run_execute_callable(context, execute, task)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 197, in wrapper\n return f(*inner_args, **inner_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 2192, in _run_execute_callable\n result = ctx.run(execute, context=context)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/operator.py\", line 462, in wrapper\n return func(self, *args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/decorator.py\", line 435, in execute\n return_value = super().execute(context)\n ^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/operator.py\", line 462, in wrapper\n return func(self, *args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/providers/standard/operators/python.py\", line 231, in execute\n return_value = self.execute_callable()\n ^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/providers/standard/operators/python.py\", line 25\n...\nfunc(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/baseline-2026-09-30-baseline-20260930-045309/debugging-manual-trigger-context-qy52po81/grade-ws/dags/partner_feed.py\", line 33, in extract_orders\n day = context[\"data_interval_start\"].strftime(\"%Y-%m-%d\")\n ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: 'data_interval_start'\n2026-09-30T12:04:55.614503Z [info ] ::group::Post Execute [task] loc=task_runner.py:1677\n2026-09-30T12:04:55.616606Z [info ] ::endgroup:: [task] loc=task_runner.py:2379\n2026-09-30T12:04:55.707966Z [info ] Task instance state updated [airflow.api_fastapi.execution_api.routes.task_instances] correlation_id=01a0f234-18c7-716f-981a-7059fea81e3c loc=task_instances.py:485 new_state=failed rows_affected=1 ti_id=01a0f234-0720-7429-9b0d-396e58a60665\n2026-09-30T12:04:55.745878Z [info ] [DAG TEST] end task task_id=extract_orders map_index=-1 [airflow.sdk.definitions.dag] loc=dag.py:1563\n2026-09-30T12:04:55.751242Z [warning ] No tasks to run. unrunnable tasks: {} [airflow.sdk.definitions.dag] loc=dag.py:1424\n2026-09-30T12:04:56.772324Z [info ] Marking run failed [airflow.models.dagrun.DagRun] loc=dagrun.py:1219\n2026-09-30T12:04:56.772781Z [info ] DagRun Finished: dag_id=partner_feed, logical_date=None, run_id=manual__2026-03-03T00:30:00+00:00_8mXpVgjc, run_start_date=2026-03-04 09:00:00+00:00, run_end_date=2026-09-30 12:04:56.772419+00:00, run_duration=18155096.772419, state=failed, run_type=manual, data_interval_start=None, data_interval_end=None, [airflow.models.dagrun.DagRun] loc=dagrun.py:1315\n__EVAL_PROBE_RESULT__{\"state\": \"failed\", \"logical_date\": null, \"run_after\": \"2026-03-03T00:30:00+00:00\"}" + }, + { + "name": "manual run delivers the feed for its trigger day (2026-03-03)", + "kind": "primary", + "passed": false, + "detail": "problems=['feed', 'manifest', 'outbox_feed', 'outbox_manifest']; expected 6 rows + manifest {'partition': '2026-03-03', 'row_count': 6}; got {\"feed\": \"missing orders.csv\", \"manifest\": \"missing manifest.json\", \"outbox_feed\": \"missing acme_orders_20260303.csv\", \"outbox_manifest\": \"missing acme_orders_20260303.manifest.json\"}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "7 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.7142857142857143, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch new file mode 100644 index 0000000..084ffc6 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/agent.patch @@ -0,0 +1,28 @@ +diff --git a/dags/partner_feed.py b/dags/partner_feed.py +index fffc488..8532cdf 100644 +--- a/dags/partner_feed.py ++++ b/dags/partner_feed.py +@@ -30,7 +30,10 @@ FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + def partner_feed(): + @task + def extract_orders(**context) -> str: +- day = context["logical_date"].strftime("%Y-%m-%d") ++ # Use data_interval_start which is present in all run types (scheduled, manual, etc.) ++ # For scheduled runs: this is the logical_date (start of the data interval) ++ # For manual triggers: this defaults to now if not specified ++ day = context["data_interval_start"].strftime("%Y-%m-%d") + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} +@@ -47,7 +50,10 @@ def partner_feed(): + + @task + def write_manifest(feed_path: str, **context) -> str: +- day = context["execution_date"].strftime("%Y-%m-%d") ++ # Use data_interval_start which is present in all run types (scheduled, manual, etc.) ++ # For scheduled runs: this is the logical_date (start of the data interval) ++ # For manual triggers: this defaults to now if not specified ++ day = context["data_interval_start"].strftime("%Y-%m-%d") + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json new file mode 100644 index 0000000..daa860a --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/attempt.json @@ -0,0 +1,32 @@ +{ + "status": "task_fail", + "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", + "primary_pass": false, + "primary_score": 0.7142857142857143, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 64.4, + "cost_usd": 0.231081, + "cost_reported_usd": 0.838345, + "tokens": { + "input": 607309, + "output": 2997, + "reasoning": 0, + "cache_read": 472199, + "cache_write": 135065, + "total": 1217570 + }, + "steps": 9, + "termination": { + "why_model_stopped": "explicit-done", + "why_harness_stopped": "none", + "done_reason": "explicit_done" + }, + "skill_invocations": [], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 3, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0da47a73ffeTtWuD8QPty0UHh.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json new file mode 100644 index 0000000..24f3cf4 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-haiku-4-5_20251001/run-2/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feed is present", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_catalog_sync', 'partner_feed']" + }, + { + "name": "task graph preserved (extract -> manifest -> deliver)", + "kind": "primary", + "passed": true, + "detail": "missing=[] broken=[] deps=[['extract_orders', 'write_manifest'], ['write_manifest', 'deliver']]" + }, + { + "name": "dags test with logical date 2026-03-02 succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "dated run delivers the 2026-03-02 feed", + "kind": "primary", + "passed": true, + "detail": "ok" + }, + { + "name": "manual run without a logical date succeeds", + "kind": "primary", + "passed": false, + "detail": "result={'state': 'failed', 'logical_date': None, 'run_after': '2026-03-03T00:30:00+00:00'} non-success tasks=['deliver', 'extract_orders', 'write_manifest']\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 2234, in _execute_task\n result = _run_execute_callable(context, execute, task)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 197, in wrapper\n return f(*inner_args, **inner_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 2192, in _run_execute_callable\n result = ctx.run(execute, context=context)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/operator.py\", line 462, in wrapper\n return func(self, *args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/decorator.py\", line 435, in execute\n return_value = super().execute(context)\n ^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/operator.py\", line 462, in wrapper\n return func(self, *args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/providers/standard/operators/python.py\", line 231, in execute\n return_value = self.execute_callable()\n ^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/providers/standard/operators/python.py\", line 25\n...\nfunc(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/baseline-2026-09-30-baseline-20260930-045309/debugging-manual-trigger-context-92ply51l/grade-ws/dags/partner_feed.py\", line 36, in extract_orders\n day = context[\"data_interval_start\"].strftime(\"%Y-%m-%d\")\n ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: 'data_interval_start'\n2026-09-30T12:49:25.699497Z [info ] ::group::Post Execute [task] loc=task_runner.py:1677\n2026-09-30T12:49:25.701897Z [info ] ::endgroup:: [task] loc=task_runner.py:2379\n2026-09-30T12:49:25.803453Z [info ] Task instance state updated [airflow.api_fastapi.execution_api.routes.task_instances] correlation_id=01a0f25c-d6c6-76bb-958c-8644606a9ac1 loc=task_instances.py:485 new_state=failed rows_affected=1 ti_id=01a0f25c-c433-7a7c-a4ac-24b1393ff6dc\n2026-09-30T12:49:25.870196Z [info ] [DAG TEST] end task task_id=extract_orders map_index=-1 [airflow.sdk.definitions.dag] loc=dag.py:1563\n2026-09-30T12:49:25.874905Z [warning ] No tasks to run. unrunnable tasks: {} [airflow.sdk.definitions.dag] loc=dag.py:1424\n2026-09-30T12:49:26.899214Z [info ] Marking run failed [airflow.models.dagrun.DagRun] loc=dagrun.py:1219\n2026-09-30T12:49:26.899584Z [info ] DagRun Finished: dag_id=partner_feed, logical_date=None, run_id=manual__2026-03-03T00:30:00+00:00_Qd1VtfKm, run_start_date=2026-03-04 09:00:00+00:00, run_end_date=2026-09-30 12:49:26.899295+00:00, run_duration=18157766.899295, state=failed, run_type=manual, data_interval_start=None, data_interval_end=None, [airflow.models.dagrun.DagRun] loc=dagrun.py:1315\n__EVAL_PROBE_RESULT__{\"state\": \"failed\", \"logical_date\": null, \"run_after\": \"2026-03-03T00:30:00+00:00\"}" + }, + { + "name": "manual run delivers the feed for its trigger day (2026-03-03)", + "kind": "primary", + "passed": false, + "detail": "problems=['feed', 'manifest', 'outbox_feed', 'outbox_manifest']; expected 6 rows + manifest {'partition': '2026-03-03', 'row_count': 6}; got {\"feed\": \"missing orders.csv\", \"manifest\": \"missing manifest.json\", \"outbox_feed\": \"missing acme_orders_20260303.csv\", \"outbox_manifest\": \"missing acme_orders_20260303.manifest.json\"}" + }, + { + "name": "ran verification commands", + "kind": "secondary", + "passed": false, + "detail": "3 bash commands, none verifying" + }, + { + "name": "ruff AIR clean", + "kind": "secondary", + "passed": true, + "detail": "" + }, + { + "name": "no deprecation warnings", + "kind": "secondary", + "passed": true, + "detail": "" + } + ], + "primary_pass": false, + "primary_score": 0.7142857142857143, + "secondary_score": 0.6666666666666666 +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch new file mode 100644 index 0000000..4e45212 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/agent.patch @@ -0,0 +1,47 @@ +diff --git a/dags/partner_feed.py b/dags/partner_feed.py +index fffc488..d3f5ee9 100644 +--- a/dags/partner_feed.py ++++ b/dags/partner_feed.py +@@ -8,7 +8,7 @@ from __future__ import annotations + + import csv + import json +-from datetime import datetime ++from datetime import datetime, timezone + from pathlib import Path + + from airflow.providers.standard.operators.bash import BashOperator +@@ -30,7 +30,17 @@ FEED_COLUMNS = ["order_id", "sku", "qty", "amount"] + def partner_feed(): + @task + def extract_orders(**context) -> str: +- day = context["logical_date"].strftime("%Y-%m-%d") ++ # logical_date is a datetime for scheduled/triggered-with-date runs; ++ # it is None when the DAG is triggered manually without a date. ++ # In the None case we snapshot today's UTC date so that the day is ++ # frozen in the output path — re-running this task tomorrow will still ++ # write to the original date's directory (XCom caches the return value). ++ logical_date = context.get("logical_date") ++ if logical_date is not None: ++ day = logical_date.strftime("%Y-%m-%d") ++ else: ++ day = datetime.now(timezone.utc).strftime("%Y-%m-%d") ++ + with ORDERS_CSV.open(newline="") as fh: + rows = [ + {col: row[col] for col in FEED_COLUMNS} +@@ -46,8 +56,12 @@ def partner_feed(): + return str(out) + + @task +- def write_manifest(feed_path: str, **context) -> str: +- day = context["execution_date"].strftime("%Y-%m-%d") ++ def write_manifest(feed_path: str) -> str: ++ # Derive the partition date from the feed path produced by extract_orders. ++ # This avoids any reliance on context keys that differ between Airflow ++ # versions or trigger modes, and keeps the day frozen to whatever ++ # extract_orders chose (important for manual-trigger re-runs). ++ day = Path(feed_path).parent.name # .../partner_feed//orders.csv + with open(feed_path, newline="") as fh: + row_count = sum(1 for _ in csv.DictReader(fh)) + manifest = Path(feed_path).with_name("manifest.json") diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json new file mode 100644 index 0000000..fd434d0 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/attempt.json @@ -0,0 +1,38 @@ +{ + "status": "task_fail", + "reason": "failed primary: manual run without a logical date succeeds, manual run delivers the feed for its trigger day (2026-03-03)", + "primary_pass": false, + "primary_score": 0.7142857142857143, + "secondary_score": 0.6666666666666666, + "returncode": 0, + "wall_s": 100.8, + "cost_usd": 0.728931, + "cost_reported_usd": 2.764158, + "tokens": { + "input": 678420, + "output": 3932, + "reasoning": 0, + "cache_read": 543222, + "cache_write": 135187, + "total": 1360761 + }, + "steps": 10, + "termination": { + "why_model_stopped": "stop", + "why_harness_stopped": "none", + "done_reason": "none" + }, + "skill_invocations": [ + { + "name": "debugging-dbt-errors", + "origin": "global", + "status": "completed" + } + ], + "auto_loaded_skills": [], + "airflow_skills_used": [], + "bash_commands": 1, + "edited_files": 1, + "errors": [], + "trace_path": "~/.local/share/altimate-code/traces/ses_f0dcfb246ffeoZuqc6334rQq2y.json" +} \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md new file mode 100644 index 0000000..9ee1ec9 --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/final.md @@ -0,0 +1 @@ +(no text output — tool-only response) \ No newline at end of file diff --git a/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json new file mode 100644 index 0000000..779643d --- /dev/null +++ b/evals/airflow/results/baseline-2026-09-30/runs/debugging-manual-trigger-context/google-vertex-anthropic_claude-sonnet-4-6_default/run-1/attempt-0/grade.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "DAGs import cleanly", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "DAG partner_feed is present", + "kind": "primary", + "passed": true, + "detail": "found: ['partner_catalog_sync', 'partner_feed']" + }, + { + "name": "task graph preserved (extract -> manifest -> deliver)", + "kind": "primary", + "passed": true, + "detail": "missing=[] broken=[] deps=[['extract_orders', 'write_manifest'], ['write_manifest', 'deliver']]" + }, + { + "name": "dags test with logical date 2026-03-02 succeeds", + "kind": "primary", + "passed": true, + "detail": "" + }, + { + "name": "dated run delivers the 2026-03-02 feed", + "kind": "primary", + "passed": true, + "detail": "ok" + }, + { + "name": "manual run without a logical date succeeds", + "kind": "primary", + "passed": false, + "detail": "result={'state': 'failed', 'logical_date': None, 'run_after': '2026-03-03T00:30:00+00:00'} non-success tasks=['deliver']\n return f(*inner_args, **inner_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 1414, in _prepare\n ti.render_templates(context=context, jinja_env=jinja_env)\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 197, in wrapper\n return f(*inner_args, **inner_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/execution_time/task_runner.py\", line 432, in render_templates\n self.task.render_template_fields(context, jinja_env)\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/bases/operator.py\", line 1652, in render_template_fields\n self._do_render_template_fields(self, self.template_fields, context, jinja_env, set())\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/definitions/_internal/templater.py\", line 189, in _do_render_template_fields\n rendered_content = self.render_template(value, context, jinja_env, seen_oids)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/definitions/_internal/templater.py\", line 260, in render_template\n return self._render(template, context)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/definitions/_internal/abstractoperator.py\", line 264, in _render\n return super()._render(template, context, dag=dag)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/d\n...\nleMapping[str, Any]\", context), native=False)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"~/.cache/des-evals/airflow-3.3/lib/python3.12/site-packages/airflow/sdk/definitions/context.py\", line 204, in render_template\n return \"\".join(nodes)\n ^^^^^^^^^^^^^^\n File \"