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..d6ec1a8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ logs/ # Test outputs .pytest_cache/ +.ruff_cache/ __pycache__/ *.pyc @@ -42,3 +43,24 @@ 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/**/session.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..a31b89b --- /dev/null +++ b/evals/airflow/BENCHMARK.md @@ -0,0 +1,334 @@ +# 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. + +### Post-benchmark fixes + +The benchmarked `skills/airflow` tree hash is recorded in each result's `meta.json` (and below). +The shipped skills tree differs only by these helper fixes and their documentation: version-aware context +keys (including removed `conf`), optional missing `plugins/`, template findings with `--dag-id`, scoped +parse-time checks, timetable identity and interval comparisons, mutation recovery after interrupted runs, +and exit 4 for manual runs without a legacy replay baseline. These fixes were made after the benchmark; +the reported results and numbers have not been changed or rerun against the updated skills. + +Agents could also see other processes' command lines through `ps`. A scan of the 152 reported task +transcripts (2,912 tool calls) found no access to solutions, results or cases files. Outside-process argv +access is now blocked through both process-info and `kern.proc` sysctl restrictions; the sandbox also +denies the whole checkout and the evaluation cache, with explicit allowances for each attempt's inputs. + +## 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 | +| Benchmarked `skills/airflow` sha256 | `d3af9fac9ff0337042ffb634539cd96828c2f63020d1651fee7fe70fd09557a0` (identical in the campaign state file before and after the campaign and in every skill-arm `meta.json`; computed by `run_eval.hash_dir`, bytecode ignored; shipped helper fixes are listed above) | +| 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 0000000..ab0215b Binary files /dev/null and b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-27.csv.gz differ diff --git a/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-28.csv.gz b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-28.csv.gz new file mode 100644 index 0000000..44d6633 Binary files /dev/null and b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-28.csv.gz differ 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 0000000..efb8215 Binary files /dev/null and b/evals/airflow/cases/authoring-clickstream-claim-check/fixture/data/clickstream/2026-09-29.csv.gz differ 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 \"