Collapse the two model-resolution stacks into one resolver
Before · two parallel stacks
flowchart TD cmdA["11 commands
schema/config/context…"] --> B["BaseCommand
get_model_with_fallback"] B --> F["FallbackStrategy
prod→dev→BQ"] SC["SchemaCommand"] -. copy-pastes .-> B cmdB["columns command"] --> SD["ModelStateDetector"] SD --> MS["detect_model_state
14-state tree"] SD --> CF["ColumnSourceFactory"] CF --> BQ["BigQuery / Catalog
source"] B -. dev-schema override .-> INV1(("schema=
dev")):::leak SD -. dev-schema override .-> INV2(("schema=
dev")):::leak SC -. dev-schema override .-> INV3(("schema=
dev")):::leak classDef leak fill:#fee2e2,stroke:#dc2626,stroke-width:2px; class SC leak
Two disconnected answers to "where does the data come from". The dev-schema invariant is re-implemented at 5+ sites.
After · one deep resolver
ModelResolver.resolve()
returns ResolvedModel{ location, level, state }
dev-schema override · fallback ordering · state detection — all inside, once
Problem
The invariant "found in dev → use dev schema, never re-search prod" is honoured by schema.py only — generic prod-mode dev-fallback silently keeps the wrong schema.
Solution
FallbackStrategy returns its FallbackResult (with level + resolved location) to every caller; the dev-schema override lives once behind that interface.
Wins
- Locality: invariant lives in one module
- Deletes
schema.py's copy of_get_model_prod_mode - Correctness: dev schema enforced for all commands
- Collapses shallow
ColumnSourceFactory(one predicate) columns.pystops bypassing its own base class- Leverage: one resolver, N call sites
base.py:179-180 ≡ state_detector.py:74-75. schema.py:75-118 copy-pastes base.py:_get_model_prod_mode only to capture result.level. columns.py:58-60 — dead process_model stub (bypasses base fallback).