Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
256c99a
DEV-1450 stage 1: typed identity primitives (dormant)
ZmeiGorynych May 21, 2026
82078d6
DEV-1450 stage 2: typed scope + bundle, with I2 anchor-optional shape
ZmeiGorynych May 21, 2026
0c59436
DEV-1450 stage 3: extract _walk_join_chain to path_resolution
ZmeiGorynych May 21, 2026
b0b29b8
DEV-1450 stage 4: extract aggregation registry helpers (dormant)
ZmeiGorynych May 21, 2026
003918f
DEV-1450 stage 5: typed errors and slack-warning carriers
ZmeiGorynych May 21, 2026
5a2c1bd
DEV-1450 stage 6: slack normalization layer (FUNC_STYLE_AGG + MISPLAC…
ZmeiGorynych May 21, 2026
332b145
DEV-1450 stages 1+6 review: bool/Decimal disambiguation + custom-agg …
ZmeiGorynych May 21, 2026
ebef83a
DEV-1450 stage 7a.1: typed plan shapes (dormant)
ZmeiGorynych May 21, 2026
0d59f58
DEV-1450 stage 6 carry-over: activate DOT_PATH_IN_SQL
ZmeiGorynych May 21, 2026
ae2b1dc
DEV-1450 stage 7a.2: cross-model planner Protocol + isolated-CTE impl…
ZmeiGorynych May 21, 2026
a8b1cc4
DEV-1450 stage 7a.3: Mode-B Python-AST parser
ZmeiGorynych May 21, 2026
baa8c8f
DEV-1450 stage 7a.4: Mode-A sqlglot wrapper
ZmeiGorynych May 21, 2026
29b274e
DEV-1450 stage 7a.5: ExpressionBinder + FilterBinder
ZmeiGorynych May 21, 2026
5ef5048
DEV-1450 stage 7a.6: ValueRegistry, TransformLowerer, ProjectionPlanner
ZmeiGorynych May 21, 2026
89d8502
DEV-1450 stage 7a.7: multi-stage DAG planner
ZmeiGorynych May 21, 2026
7b8915b
DEV-1450 stage 7b.1: variables substitution in new pipeline
ZmeiGorynych May 21, 2026
4eef4b6
DEV-1450 stage 7b.2: pre-bind ModelMeasure expansion
ZmeiGorynych May 21, 2026
d46f114
DEV-1450 stage 7b.3a: TimeTruncKey for time-dimension identity
ZmeiGorynych May 21, 2026
7654770
DEV-1450 stage 7b.3b: bind_time_dimension + planner integration
ZmeiGorynych May 21, 2026
2a5b2a9
DEV-1450 stage 7b.3c: date_range filter + main-TD disambiguation
ZmeiGorynych May 21, 2026
8fd6167
DEV-1450 stage 7b.4: planner-side transform support (11 primitives)
ZmeiGorynych May 21, 2026
6ff18a3
DEV-1450 stage 7b.5: cross-model planner wiring + HostFilterRouting
ZmeiGorynych May 21, 2026
b65fab8
DEV-1450 stage 7b.6: BoundExpr type unification
ZmeiGorynych May 21, 2026
b8816e1
DEV-1450 stage 7b.7: parity oracle test helpers (drop adapter)
ZmeiGorynych May 21, 2026
85d7a4b
DEV-1450 stage 7b.8: local-only generator slice (generate_from_planned)
ZmeiGorynych May 21, 2026
2145e55
DEV-1450 stage 7b.9: generator slice for time dimensions + model.filters
ZmeiGorynych May 22, 2026
1faa044
DEV-1450 stage 7b.10: generator slice for window transforms
ZmeiGorynych May 22, 2026
6ca3cb4
DEV-1450 stage 7b.11: generator slice for self-join CTE transforms
ZmeiGorynych May 22, 2026
c43940a
DEV-1450 stage 7b.12: generator slice for cross-model CTEs
ZmeiGorynych May 22, 2026
3ef9288
DEV-1450 stage 7b.13: generator slice for dialect parity
ZmeiGorynych May 23, 2026
d00739a
DEV-1450 stage 7b.14: production migration off MixedArithmeticField
ZmeiGorynych May 23, 2026
cff26fc
DEV-1450 stage 7b.15a: build_resolved_source_bundle
ZmeiGorynych May 23, 2026
164d975
DEV-1450 stage 7b.15b: binder/parser/core gaps for the 4 cross-model …
ZmeiGorynych May 23, 2026
1cc6508
DEV-1450 stage 7b.15c: multi-stage DAG rendering (generate_planned_st…
ZmeiGorynych May 23, 2026
cf73d3b
DEV-1450 stage 7b.15d: engine cutover (default-path flip)
ZmeiGorynych May 24, 2026
b79c4f6
DEV-1450 stage 7b.15d: integration fixes (chain re-count + time_shift…
ZmeiGorynych May 24, 2026
855dd76
DEV-1450 stage 7b.15d: integration fixes (windowed-filter ValueError …
ZmeiGorynych May 24, 2026
188ed8b
DEV-1450 stage 7b.15d: integration fixes (join cycle detection, no-jo…
ZmeiGorynych May 24, 2026
2f3a11b
DEV-1450 stage 7b.15d: integration fixes (local HAVING for aggregate …
ZmeiGorynych May 24, 2026
64a91f3
DEV-1450 stage 7b.15d: integration fix (arithmetic precedence parens …
ZmeiGorynych May 24, 2026
c92439c
DEV-1450 stage 7b.15d: integration fix (precedence parens in filter a…
ZmeiGorynych May 24, 2026
1207bc4
DEV-1450 stage 7b.15d: integration fix (first/last aggregation in pla…
ZmeiGorynych May 24, 2026
21fe98e
DEV-1450 stage 7b.15d: integration fix (composite-input window transf…
ZmeiGorynych May 24, 2026
7ffe64a
DEV-1450 stage 7b.15d: integration fix (derived columns + joined-colu…
ZmeiGorynych May 24, 2026
c00356a
DEV-1450 stage 7b.15d: integration fix (cross-model re-rooted CTE for…
ZmeiGorynych May 24, 2026
825b182
DEV-1450 stage 7b.15d: integration fix (window transform over cross-m…
ZmeiGorynych May 24, 2026
d90738d
DEV-1450 stage 7b.15e: acceptance tests via engine.execute (1445/1446…
ZmeiGorynych May 24, 2026
5601ad5
DEV-1450 stage 7b.15e: safe deletes (legacy multidot rewriter + parit…
ZmeiGorynych May 24, 2026
7a2824a
DEV-1450 follow-ups: derived-column TDs (#4a), derived-column model f…
ZmeiGorynych May 24, 2026
c1bdd75
DEV-1450 review fixes (group 3): green the Sonar reliability gate
ZmeiGorynych May 24, 2026
44047b0
DEV-1450 review fixes (group 1): cross-model derived-column completeness
ZmeiGorynych May 24, 2026
1eae2f5
DEV-1450 review fixes (group 2): correctness bugs from CodeRabbit
ZmeiGorynych May 24, 2026
2d9729e
DEV-1450 review fixes (group 4): Sonar/CodeRabbit maintainability qui…
ZmeiGorynych May 24, 2026
9e7e59e
DEV-1450 review fixes (round 2): derived agg sources, drift scope, OV…
ZmeiGorynych May 24, 2026
91d7ca2
DEV-1450: merge origin/main, reconcile tests with typed pipeline cont…
ZmeiGorynych May 26, 2026
9b36e71
DEV-1452 Stage A: AggRenderSpec — decouple dialect helpers from Enric…
ZmeiGorynych May 26, 2026
b7ba22b
DEV-1450: PR #139 review pass — Codex + CodeRabbit fixes + CI gap defer
ZmeiGorynych May 26, 2026
ee18009
Merge remote-tracking branch 'origin/egor/dev-1450-principled-redesig…
ZmeiGorynych May 26, 2026
b67eecb
DEV-1452 review fixes (group 1): ColumnSqlKey time arg + complexity
ZmeiGorynych May 26, 2026
a4f33c3
DEV-1452 review fixes (group 2): split _build_ranked_subquery_from_pl…
ZmeiGorynych May 26, 2026
9b32915
DEV-1452 review fixes (group 3): S8396 nullable idiom + DEV-1476 ref
ZmeiGorynych May 26, 2026
3c76c95
DEV-1452 review fixes (group 4): CodeRabbit outside-diff folds
ZmeiGorynych May 26, 2026
7a1110f
DEV-1450: PR #139 round 2 — complete IS/IS NOT coverage + n-ary boole…
ZmeiGorynych May 26, 2026
f1fd662
DEV-1452 review fixes (group 5): S3776 hybrid — helpers + NOSONAR
ZmeiGorynych May 27, 2026
09ab5bc
DEV-1452 Stage B: migrate query-backed expansion onto typed pipeline
ZmeiGorynych May 27, 2026
18daf82
DEV-1452 Stage B review fixes (group 1): Codex correctness — slot met…
ZmeiGorynych May 27, 2026
43fa00e
DEV-1452 Stage B review fixes (group 2): Sonar quick wins
ZmeiGorynych May 27, 2026
feb76af
DEV-1452 Stage B review fixes (group 3): S3776 NOSONAR fan-out
ZmeiGorynych May 27, 2026
eb2e6f8
DEV-1452 Stage B round 2 (group 1): Codex correctness — nested vars +…
ZmeiGorynych May 27, 2026
6ca71e9
DEV-1452 Stage B round 2 (group 2): S3776 _merge_into_existing — extr…
ZmeiGorynych May 27, 2026
351e9b2
DEV-1452 Stage B round 3: Codex — thread bundle through all 5 missing…
ZmeiGorynych May 27, 2026
cde6293
DEV-1452 Stage B round 4: Codex — sibling stage var-layer fix
ZmeiGorynych May 27, 2026
c082ae2
DEV-1452 Stage B round 5: Codex — run-by-name topo + saved ModelMeasu…
ZmeiGorynych May 27, 2026
482e243
DEV-1475: Mode-B IN / NOT IN filters with literal-tuple RHS
ZmeiGorynych May 27, 2026
e67a3de
Merge branch 'egor/dev-1450-principled-redesign-of-syntax' of https:/…
ZmeiGorynych May 27, 2026
c2b7f45
DEV-1452 Stage B round 6: Codex + CodeRabbit fixes
ZmeiGorynych May 27, 2026
5a6ac64
DEV-1452 Stage B round 7: Codex — thread filtered rank maps into cros…
ZmeiGorynych May 28, 2026
d2c32bb
DEV-1452 Stage B fix-up: binding-time errors must subclass ValueError
ZmeiGorynych May 28, 2026
37a0417
DEV-1484 Stage C (commit 1): migrate simpler test files off legacy en…
ZmeiGorynych May 28, 2026
be3aa03
DEV-1484 Stage C (commit 2): migrate reference-semantics + formula tests
ZmeiGorynych May 28, 2026
0c328bd
DEV-1484 Stage C (commit 3): drop legacy resolver unit tests + migrat…
ZmeiGorynych May 28, 2026
0a5aeb1
DEV-1484 Stage C (commit 4a): migrate cross-model derived-column test…
ZmeiGorynych May 28, 2026
1d16e7e
DEV-1484 Stage C (commit 4b): migrate test_projection_trim off legacy…
ZmeiGorynych May 28, 2026
732891b
DEV-1484 Stage C (commit 4c): scrub legacy-symbol references from tes…
ZmeiGorynych May 28, 2026
4ccb673
DEV-1452 Stage B round 8: Codex — joined-dim type + derived time-col …
ZmeiGorynych May 28, 2026
7b03363
DEV-1484 Stage C (commit 5a): migrate test_sql_generator off legacy (…
ZmeiGorynych May 28, 2026
8b2239c
Merge pull request #144 from MotleyAI/egor/dev-1452-delete-legacy-enr…
ZmeiGorynych May 28, 2026
fea8381
DEV-1484 Stage C (commit 6): complete test_sql_generator migration of…
ZmeiGorynych May 28, 2026
0065024
Merge remote-tracking branch 'origin/egor/dev-1450-principled-redesig…
ZmeiGorynych May 28, 2026
f083fbe
DEV-1494: inline cross-model derived refs in Column.filter + discover…
ZmeiGorynych May 29, 2026
db888d0
DEV-1494: address review feedback (CodeRabbit + Codex + Sonar gate)
ZmeiGorynych May 29, 2026
b343231
DEV-1494: clear remaining Sonar gate findings
ZmeiGorynych May 29, 2026
40d31b1
Merge branch 'main' of https://github.com/MotleyAI/slayer into egor/d…
ZmeiGorynych May 30, 2026
74ed95a
Merge pull request #155 from MotleyAI/egor/dev-1494-typed-pipeline-cr…
ZmeiGorynych May 30, 2026
beb6924
DEV-1500: recognize joined-model custom aggregations in FUNC_STYLE_AG…
ZmeiGorynych May 30, 2026
f7a24e3
DEV-1492: preserve transform kwargs in filter normalization
ZmeiGorynych May 31, 2026
1714df3
DEV-1501: parametric first/last in ORDER BY / HAVING — materialise + …
ZmeiGorynych May 31, 2026
8bc9504
DEV-1500: also recognize joined-model custom aggs in the quiet FUNC_S…
ZmeiGorynych May 31, 2026
dc7a775
DEV-1501 PR #159 review fixes: HAVING ranked-state threading + Sonar …
ZmeiGorynych May 31, 2026
74f2b66
Refactor: extract _chain_model + _save_4hop_chain helpers
ZmeiGorynych May 31, 2026
00a0a7a
DEV-1501 PR #159 review round 2: thread aliases through nested HAVING…
ZmeiGorynych May 31, 2026
ca84ccc
DEV-1501 PR #159 review round 3: composite first/last in projection r…
ZmeiGorynych May 31, 2026
c8fbfc5
Refactor: split normalize_model + extract save-time agg walk to a method
ZmeiGorynych May 31, 2026
93bc7cd
Merge pull request #156 from MotleyAI/egor/dev-1500-slack-func_style_…
ZmeiGorynych May 31, 2026
5073ee8
DEV-1501 PR #159 review round 4: composite-only first/last triggers r…
ZmeiGorynych May 31, 2026
248fee1
syntax: split _rewrite_comparison_equals to clear cognitive-complexit…
ZmeiGorynych May 31, 2026
a086de3
DEV-1501 PR #159 review round 5: composite first/last validation + jo…
ZmeiGorynych Jun 1, 2026
5568111
syntax: restore transform predicate-input form (bare-ref equality)
ZmeiGorynych Jun 1, 2026
426f8ad
DEV-1501 PR #159 review round 6: filtered composite + cross-model AGG…
ZmeiGorynych Jun 1, 2026
3527a09
DEV-1501 PR #159 review round 7: ScalarCallKey alias thread + dedupe …
ZmeiGorynych Jun 1, 2026
8eab620
syntax: colon-agg context disambiguates ambiguous transform/agg names
ZmeiGorynych Jun 1, 2026
a66e0e8
DEV-1501 PR #159 review round 8: derived time-col join + test dedup
ZmeiGorynych Jun 1, 2026
b1b8b09
Merge pull request #158 from MotleyAI/egor/dev-1492-parse_filter_expr…
ZmeiGorynych Jun 1, 2026
85e13c9
DEV-1501 PR #159 review round 9: cross-model HAVING args/kwargs reroot
ZmeiGorynych Jun 1, 2026
ffd85bc
Merge pull request #159 from MotleyAI/egor/dev-1501-parametric-lastfi…
ZmeiGorynych Jun 1, 2026
5aea9b0
DEV-1503: filtered-local isolation on typed pipeline + outer-WHERE wr…
ZmeiGorynych Jun 3, 2026
17d9ede
DEV-1503 review round 1: filter routing, mixed cross-model wrapper, h…
ZmeiGorynych Jun 3, 2026
1cf7ff6
DEV-1503 review round 2: composite-over-isolated + no-dim placeholder
ZmeiGorynych Jun 3, 2026
c81ab51
DEV-1503 review round 3: outer-composite routing edge cases
ZmeiGorynych Jun 3, 2026
ec90f1c
DEV-1503 review round 4: empty_base host filters + Sonar S3776
ZmeiGorynych Jun 3, 2026
3ec2fb9
DEV-1503 review round 5: skip_filter_ids in first/last base path
ZmeiGorynych Jun 3, 2026
f422473
DEV-1503 review round 5b: empty_base host filter joined-alias discovery
ZmeiGorynych Jun 3, 2026
2f22c17
DEV-1503 review round 7: dialect fallback chain on filter parse
ZmeiGorynych Jun 3, 2026
71e28f8
DEV-1503 review round 4: empty_base host filters + Sonar S3776
ZmeiGorynych Jun 3, 2026
0bd34f3
DEV-1503 review round 9: dialect fallback covers derived-ref expansion
ZmeiGorynych Jun 3, 2026
e381429
Merge pull request #163 from MotleyAI/egor/dev-1503-typed-pipeline-la…
ZmeiGorynych Jun 3, 2026
30e5e09
DEV-1502: pull joins crossed by an aggregate source's Column.sql
ZmeiGorynych Jun 3, 2026
fc0853f
DEV-1502: use pytest.approx for float SUM assertion (Sonar S1244)
ZmeiGorynych Jun 4, 2026
b8aa68d
DEV-1502: strengthen cross-model defensive test's negative assertion
ZmeiGorynych Jun 4, 2026
7862290
DEV-1502: qualify cross-model CTE join-discovery note (Codex)
ZmeiGorynych Jun 4, 2026
e04620f
DEV-1502: convert first/last derived-source test to DEV-1531 xfail
ZmeiGorynych Jun 4, 2026
1c69c78
Merge pull request #165 from MotleyAI/egor/dev-1502-typed-pipeline-__…
ZmeiGorynych Jun 5, 2026
a168d64
Merge branch 'refs/heads/egor/dev-1450-principled-redesign-of-syntax'…
ZmeiGorynych Jun 12, 2026
4278bf8
PR #153 review sweep: 5 correctness bugs + 3 keyword-only helpers + t…
ZmeiGorynych Jun 12, 2026
ed44862
PR #153 follow-up: Codex lexer fix + 3 S3776 refactors + CI for stack…
ZmeiGorynych Jun 12, 2026
2c48889
PR #153 round-3 review: 2 CI failures fixed + lexer tests + S3776 < 15
ZmeiGorynych Jun 12, 2026
e62f913
PR #153 round-3 follow-up: refactor 4 pre-existing S3776 functions in…
ZmeiGorynych Jun 12, 2026
b6edfe8
Merge pull request #153 from MotleyAI/egor/dev-1484-dev-1452-stage-c-…
ZmeiGorynych Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
# Run CI for every PR regardless of base branch — stacked PRs
# targeting feature branches (e.g. `egor/dev-1450-…`) need the
# same lint + test gate as PRs to main.
branches: ['**']

jobs:
lint-and-test:
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

165 changes: 165 additions & 0 deletions docs/architecture/binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Binding

**Module:** `slayer/engine/binding.py`

The binder takes a `ParsedExpr`, a scope (`ModelScope` or `StageSchema`), and a
`ResolvedSourceBundle`, and produces a `BoundExpr` whose leaves are resolved
`ValueKey`s. It is the stage that turns *names* into *structural identity* — and
it is a pure function of its inputs (P11).

```mermaid
flowchart LR
parsed["ParsedExpr"] --> bind["_bind (recursive)"]
scope["scope: ModelScope | StageSchema"] --> bind
bundle["ResolvedSourceBundle"] --> bind
bind --> bk["BoundExpr(value_key: ValueKey)"]
```

## Output types

- `BoundExpr(value_key)` — the whole expression's structural identity. `.phase`
is lifted from `value_key.phase`.
- `BoundFilter(value_key, phase, referenced_keys)` — adds the max phase any
referenced slot reaches (**P8**) and the full tuple of `ValueKey`s touched
anywhere in the tree (used by cross-model filter routing).

Public entry points: `bind_expr`, `bind_filter`, `bind_time_dimension`, plus the
`walk_value_keys` traversal helper.

## Resolving a reference

`_resolve_ref` (bare) and `_resolve_dotted` (path) are where scope semantics
live (**P5**):

**Against a `StageSchema`:** a bare name must be a column in the flat schema
(else `UnknownReferenceError`); a dotted ref raises `IllegalScopeReferenceError`
("downstream stages see a flat schema"). This is the DEV-1449 guard.

**Against a `ModelScope`:**

- A bare name resolves to a `ColumnKey(path=(), leaf=name)`, or to a
`ColumnSqlKey` if the column is derived (`col.sql` set and not a trivial
self-remap). If the name matches a `ModelMeasure` instead of a column, the
binder raises with a suggestion to expand it first — measure expansion is the
[parser stage](parsing.md)'s job, not the binder's.
- A `__`-bearing bare name is legal *only* if it exact-matches a literal column
name (the C11 carve-out); otherwise `IllegalScopeReferenceError`.
- A dotted ref walks the join graph: `parts[:-1]` are join targets, `parts[-1]`
is the leaf. Each hop must have a matching `ModelJoin` on the current model and
the target must be in the bundle; revisiting a model raises a legacy-compatible
`Circular join` error. The terminal column becomes a `ColumnKey` (or
`ColumnSqlKey`) carrying the hop path.

### C14 — self-prefix stripping

`_resolve_dotted` strips a leading segment equal to the host model's name before
walking: `orders.status` on an `orders`-rooted query → `status`;
`orders.customers.name` → `customers.name`. This is principle **C14**,
preserving the legacy convenience of qualifying with your own model name.

## Binding aggregates

`_bind_agg` builds an `AggregateKey`. The source is a `StarKey` for `*`, a
path-carrying `StarKey` for a cross-model star (`customers.*:count`, via
`_resolve_dotted_star`), or the bound column otherwise. Positional/kwarg
arguments bind through `_bind_agg_arg` — identifier args become `ColumnKey`,
literals normalize via `normalize_scalar`. Crucially, `_resolve_column_filter_key`
looks up the resolved source column's `Column.filter` and folds it into the key
as `column_filter_key` (a `SqlExprKey`) — so an aggregate over a filtered column
has a distinct identity (P3 / the `column_filter_key` invariant from
[Typed keys](typed-keys.md)).

## Binding transforms (P9)

`_bind_transform` produces a `TransformKey` whose `input` is the bound value to
transform — a `ValueSlotRef`, not a string. Two whitelists govern it:

- `_TRANSFORM_KWARG_RULES` — per-op accepted kwargs. It is deliberately broader
than the legacy whitelist: every transform implicitly accepts `partition_by`
(so `change(measure, partition_by=…)` threads through to the desugared
`time_shift`, **C6**), and the rank family / `time_shift` / `lag` / `lead` /
`ntile` / `consecutive_periods` add their own.
- `_TRANSFORM_POSITIONAL_KWARGS` — the transforms whose documented DSL form
accepts positional params after the value (`time_shift(x, periods,
granularity)`, `lag(x, periods)`, `lead(x, periods)`). A name supplied both
positionally and as a kwarg is an error.

`partition_by` values must bind to a `ColumnKey` / `ColumnSqlKey` (and become
`partition_keys`); other kwargs must fold to a scalar literal
(`_fold_to_scalar` also handles unary-minus over a numeric literal, the AST shape
of `periods=-1`). `_apply_transform_kwarg_defaults` validates required kwargs
(`ntile` needs a positive-integer `n`; `time_shift` needs `periods`) and applies
defaults (`lag`/`lead` default `periods=1`). Note the binder does **not** set
`time_key` — it lacks query context; the [stage planner](stage-planning.md)
attaches it after all binding completes.

## Binding scalar calls (P1 / C12)

`_bind_scalar` re-checks `SCALAR_FUNCTIONS` membership (defence-in-depth against
direct `ParsedExpr` construction that bypasses the parser) and builds a
`ScalarCallKey`. Arithmetic, comparison, boolean, and unary ops all become
`ArithmeticKey` with the operator string.

## Filters and phase classification (P8)

`bind_filter` binds the predicate, walks it via `walk_value_keys` to gather every
referenced `ValueKey`, takes `phase = max(referenced phases)`, and rejects raw
windows. The phase is computed entirely from the slots referenced — no text
analysis. `_reject_windowed_column_sql` raises `IllegalWindowInFilterError` if
any referenced `ColumnSqlKey` has a windowed `Column.sql` body — DEV-1369
removed predicate promotion, so filtering on a window is an error, not an
auto-hoist. (Against a `StageSchema` this check is skipped — window detection
already happened when the upstream stage was bound.)

### `alias_map` — filter/order refs by declared name (P4 / DEV-1445)

`bind_filter` accepts an optional `alias_map: Dict[str, ValueKey]` mapping a
stage's declared-measure names (user `name`, declared name, canonical alias) to
their bound `ValueKey`. A bare ref matching an alias interns onto that exact slot
*before* any column lookup. This is what lets a filter reference a renamed measure
by alias: `filters=["rev >= 100"]` for a measure declared
`{"formula": "customers.revenue:sum", "name": "rev"}` binds `rev` onto the
cross-model aggregate slot — and because the dotted/colon form interns
structurally onto the *same* `AggregateKey`, both forms share one slot. Only
*measure* aliases enter the map (never dimension / time-dimension names), because
a time dimension's declared name is its raw column and a `created_at <= '…'`
filter must resolve to the raw column, not the truncated dimension slot. See
[Stage planning](stage-planning.md) for how the map is built.

## `bind_time_dimension`

Binds a `TimeDimension` into a `BoundExpr` carrying a `TimeTruncKey`. The
underlying column resolves against scope exactly like an identifier ref and must
have a temporal `Column.type` (`DATE` / `TIMESTAMP`). It may be a base
`ColumnKey` OR a **derived** `ColumnSqlKey` (DEV-1450 follow-up #4a):
`TimeTruncKey.column` is `Union[ColumnKey, ColumnSqlKey]`, and the generator
applies the `DATE_TRUNC` over the expanded `Column.sql` everywhere it would
over a bare column.

> **Limitation.** Only `ModelScope` is accepted (a `StageSchema` raises —
> downstream stages already see the truncated column as a flat name).

## `walk_value_keys`

Yields every `ValueKey` reachable from a key (including the key itself),
recursing through `AggregateKey` source/args/kwargs, `TransformKey`
input/args/kwargs/partition_keys/time_key, `ArithmeticKey` operands,
`ScalarCallKey` args, and `BetweenKey` column/low/high. It is the typed
counterpart to the parser's `walk_parsed_refs`, used for phase computation and
cross-model filter routing.

## Design rationale

- **Why is the binder pure?** Everything it needs is in the bundle (P11). No
storage access, no `ContextVar`, no callback re-resolution — so binding a given
`(parsed, scope, bundle)` is deterministic and order-independent. This is the
single biggest simplification over the legacy enrichment closures.
- **Why fold `Column.filter` into the aggregate key rather than handle it at
render time?** Because two aggregates over the same column differ *as values*
when their filters differ; making that part of identity means the registry
interns correctly and the generator never has to reconcile two slots that are
"the same column but filtered differently".
- **Why does the binder leave `time_key` unset?** Resolving the active time
dimension needs query-level context (`main_time_dimension`,
`default_time_dimension`, the set of TDs in the query). The binder is
expression-local; the planner has the query, so it patches `time_key` there.
Loading