---
phase: 38-module-directory-splits
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/expand/mod.rs
  - src/expand/types.rs
  - src/expand/resolution.rs
  - src/expand/facts.rs
  - src/expand/fan_trap.rs
  - src/expand/role_playing.rs
  - src/expand/join_resolver.rs
  - src/expand/sql_gen.rs
  - src/expand/test_helpers.rs
autonomous: true
requirements: [REF-01, REF-05]

must_haves:
  truths:
    - "src/expand/ exists as a module directory with mod.rs and 7 submodules"
    - "src/expand.rs no longer exists (deleted to avoid E0761)"
    - "All external consumers compile without import changes"
    - "All 86 expand tests pass in their new submodule locations"
    - "cargo test passes with zero behavior changes"
  artifacts:
    - path: "src/expand/mod.rs"
      provides: "Re-exports preserving exact prior public API"
      contains: "pub use types::{ExpandError, QueryRequest}"
    - path: "src/expand/types.rs"
      provides: "QueryRequest struct, ExpandError enum, Display impl"
      contains: "pub struct QueryRequest"
    - path: "src/expand/resolution.rs"
      provides: "quote_ident, quote_table_ref, find_dimension, find_metric"
      contains: "pub fn quote_ident"
    - path: "src/expand/facts.rs"
      provides: "Fact inlining, toposort, derived metric collection"
      contains: "pub(crate) fn collect_derived_metric_source_tables"
    - path: "src/expand/fan_trap.rs"
      provides: "Fan trap detection, ancestors_to_root"
      contains: "pub(crate) fn ancestors_to_root"
    - path: "src/expand/role_playing.rs"
      provides: "Role-playing dimension resolution"
      contains: "fn find_using_context"
    - path: "src/expand/join_resolver.rs"
      provides: "PK/FK join synthesis and resolution"
      contains: "fn resolve_joins_pkfk"
    - path: "src/expand/sql_gen.rs"
      provides: "Main expand() entry point"
      contains: "pub fn expand"
    - path: "src/expand/test_helpers.rs"
      provides: "Shared make_def() helper for expand tests"
      contains: "pub(super) fn make_def"
  key_links:
    - from: "src/expand/mod.rs"
      to: "src/expand/types.rs"
      via: "pub use types::{ExpandError, QueryRequest}"
      pattern: "pub use types::"
    - from: "src/expand/mod.rs"
      to: "src/expand/resolution.rs"
      via: "pub use resolution::{quote_ident, quote_table_ref}"
      pattern: "pub use resolution::"
    - from: "src/expand/mod.rs"
      to: "src/expand/sql_gen.rs"
      via: "pub use sql_gen::expand"
      pattern: "pub use sql_gen::expand"
    - from: "src/expand/mod.rs"
      to: "src/expand/facts.rs"
      via: "pub(crate) use facts::collect_derived_metric_source_tables"
      pattern: "pub\\(crate\\) use facts::"
    - from: "src/expand/mod.rs"
      to: "src/expand/fan_trap.rs"
      via: "pub(crate) use fan_trap::ancestors_to_root"
      pattern: "pub\\(crate\\) use fan_trap::"
    - from: "src/expand/sql_gen.rs"
      to: "crate::graph::RelationshipGraph"
      via: "use crate::graph::RelationshipGraph"
      pattern: "use crate::graph::RelationshipGraph"
    - from: "src/query/explain.rs"
      to: "src/expand/mod.rs"
      via: "use crate::expand::{expand, QueryRequest}"
      pattern: "use crate::expand::\\{expand, QueryRequest\\}"
    - from: "src/query/error.rs"
      to: "src/expand/mod.rs"
      via: "use crate::expand::ExpandError"
      pattern: "use crate::expand::ExpandError"
---

<objective>
Split src/expand.rs (4,299 lines) into a module directory src/expand/ with 7 single-responsibility submodules, preserving the exact public API surface via mod.rs re-exports.

Purpose: Decompose the largest source file into navigable, single-responsibility submodules. This is a purely mechanical, behavior-preserving refactoring -- no logic changes.
Output: src/expand/ directory with mod.rs, 7 submodules (types, resolution, facts, fan_trap, role_playing, join_resolver, sql_gen), and test_helpers.rs. Old src/expand.rs deleted.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/38-module-directory-splits/38-RESEARCH.md

@src/expand.rs (the file being split -- read in full before any changes)
@src/lib.rs (declares pub mod expand -- no changes needed, Rust resolves to directory)

<interfaces>
<!-- External consumers that MUST continue to compile without changes -->

From src/query/explain.rs:
  use crate::expand::{expand, QueryRequest};

From src/query/table_function.rs:
  use crate::expand::{expand, QueryRequest};

From src/query/error.rs:
  use crate::expand::ExpandError;

From src/ddl/show_dims_for_metric.rs:
  use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables};

From src/expand.rs internal (will become cross-submodule):
  use crate::graph::RelationshipGraph;  -- used by join_resolver.rs and sql_gen.rs
  use crate::model::{Cardinality, Fact, Join, SemanticViewDefinition, TableRef};
  use crate::util::{is_word_boundary_char, replace_word_boundary, suggest_closest};
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create expand/ production code submodules and mod.rs</name>
  <files>
    src/expand/mod.rs
    src/expand/types.rs
    src/expand/resolution.rs
    src/expand/facts.rs
    src/expand/fan_trap.rs
    src/expand/role_playing.rs
    src/expand/join_resolver.rs
    src/expand/sql_gen.rs
  </files>
  <read_first>
    src/expand.rs (FULL read -- this is the source of all code being moved)
    src/lib.rs (verify pub mod expand declaration -- no changes needed)
    src/query/error.rs (verify ExpandError import path)
    src/query/explain.rs (verify expand/QueryRequest import path)
    src/ddl/show_dims_for_metric.rs (verify ancestors_to_root/collect_derived_metric_source_tables import paths)
  </read_first>
  <action>
    Create src/expand/ directory. Create each submodule file by extracting the corresponding lines from expand.rs. Then delete src/expand.rs. The exact decomposition:

    **src/expand/types.rs** -- Lines 1-165 of expand.rs (types and error definitions):
    - Move: `pub struct QueryRequest` (line 16), `pub enum ExpandError` (line 23), `impl fmt::Display for ExpandError` (line 64), `impl std::error::Error for ExpandError` (line 152)
    - Imports needed: `use std::fmt;`
    - All items stay `pub` (they are re-exported from mod.rs)

    **src/expand/resolution.rs** -- Lines 167-248 of expand.rs (quoting and lookup):
    - Move: `pub fn quote_ident()` (line 167), `pub fn quote_table_ref()` (line 180), `fn find_dimension()` (line 193), `fn find_metric()` (line 225)
    - Change `find_dimension` and `find_metric` from private `fn` to `pub(super) fn` (needed by sql_gen.rs)
    - Imports needed: `use crate::model::{SemanticViewDefinition, Dimension, Metric};`
    - Keep `quote_ident` and `quote_table_ref` as `pub fn` (re-exported from mod.rs)
    - Preserve doc-comments and `#[must_use]` attributes exactly

    **src/expand/facts.rs** -- Lines 391-928 of expand.rs (fact inlining and derived metrics):
    - Move: `fn collect_derived_metric_using()` (line 391), `fn toposort_facts()` (line 605), `fn inline_facts()` (line 684), `fn toposort_derived()` (line 736), `fn inline_derived_metrics()` (line 817), `pub(crate) fn collect_derived_metric_source_tables()` (line 868)
    - Change to `pub(super)`: `collect_derived_metric_using`, `toposort_facts`, `inline_facts`, `inline_derived_metrics`
    - Keep `toposort_derived` as private (only used within facts.rs by inline_derived_metrics)
    - Keep `collect_derived_metric_source_tables` as `pub(crate)` (re-exported from mod.rs)
    - Imports needed: `use std::collections::{HashMap, HashSet, VecDeque};`, `use crate::model::{Fact, SemanticViewDefinition, TableRef};`, `use crate::util::{is_word_boundary_char, replace_word_boundary, suggest_closest};`

    **src/expand/fan_trap.rs** -- Lines 942-1157 of expand.rs (fan trap detection):
    - Move: `fn check_fan_traps()` (line 942), `pub(crate) fn ancestors_to_root()` (line 1044), `fn path_from_ancestor_to_node()` (line 1056), `fn check_path_up()` (line 1077), `fn check_path_down()` (line 1124)
    - Change `check_fan_traps` to `pub(super)` (needed by sql_gen.rs)
    - Keep `ancestors_to_root` as `pub(crate)` (re-exported from mod.rs)
    - Keep `path_from_ancestor_to_node`, `check_path_up`, `check_path_down` as private
    - Imports needed: `use std::collections::HashMap;`, `use crate::model::{Cardinality, SemanticViewDefinition, TableRef};`, `use super::types::ExpandError;`, `use super::facts::collect_derived_metric_source_tables;`

    **src/expand/role_playing.rs** -- Lines 303-452 of expand.rs (role-playing dimensions):
    - Move: `fn relationships_to_table()` (line 303), `fn find_using_context()` (line 323)
    - Keep `relationships_to_table` as private (only used within role_playing.rs)
    - Change `find_using_context` to `pub(super)` (needed by sql_gen.rs)
    - Imports needed: `use crate::model::{SemanticViewDefinition, TableRef};`, `use crate::util::suggest_closest;`, `use super::types::ExpandError;`, `use super::facts::collect_derived_metric_using;`

    **src/expand/join_resolver.rs** -- Lines 255-596 of expand.rs (join synthesis):
    - Move: `fn synthesize_on_clause()` (line 255), `fn synthesize_on_clause_scoped()` (line 268), `fn resolve_joins_pkfk()` (line 464)
    - Change all three to `pub(super)` (needed by sql_gen.rs)
    - Imports needed: `use std::collections::{HashMap, HashSet, VecDeque};`, `use crate::model::{Cardinality, Join, SemanticViewDefinition, TableRef};`, `use crate::graph::RelationshipGraph;`, `use super::facts::{collect_derived_metric_source_tables, collect_derived_metric_using};`, `use super::role_playing::{relationships_to_table, find_using_context};`, `use super::resolution::quote_ident;`
    - Note: `relationships_to_table` in role_playing.rs must also be `pub(super)` since join_resolver uses it. Update role_playing.rs accordingly.

    **src/expand/sql_gen.rs** -- Lines 1172-1365 of expand.rs (main expand function):
    - Move: `pub fn expand()` (line 1172)
    - Keep as `pub fn` (re-exported from mod.rs)
    - Imports needed: `use std::collections::{HashMap, HashSet};`, `use crate::model::{Cardinality, SemanticViewDefinition, TableRef};`, `use crate::util::suggest_closest;`, `use crate::graph::RelationshipGraph;`, `use super::types::{ExpandError, QueryRequest};`, `use super::resolution::{find_dimension, find_metric, quote_ident, quote_table_ref};`, `use super::facts::{toposort_facts, inline_facts, inline_derived_metrics, collect_derived_metric_source_tables};`, `use super::fan_trap::check_fan_traps;`, `use super::role_playing::find_using_context;`, `use super::join_resolver::{synthesize_on_clause, synthesize_on_clause_scoped, resolve_joins_pkfk};`

    **src/expand/mod.rs** -- Module declarations and re-exports:
    ```rust
    mod types;
    mod resolution;
    mod facts;
    mod fan_trap;
    mod join_resolver;
    mod role_playing;
    mod sql_gen;

    #[cfg(test)]
    mod test_helpers;

    // Public API (matches prior expand.rs surface exactly)
    pub use types::{ExpandError, QueryRequest};
    pub use resolution::{quote_ident, quote_table_ref};
    pub use sql_gen::expand;

    // Crate-internal API
    pub(crate) use facts::collect_derived_metric_source_tables;
    pub(crate) use fan_trap::ancestors_to_root;
    ```

    **Delete src/expand.rs** after all submodule files are created. This MUST happen in the same commit -- Rust E0761 forbids both src/expand.rs and src/expand/mod.rs existing simultaneously.

    **Critical visibility adjustments** (functions that were private in the monolith but are now used across submodules):
    - `find_dimension`, `find_metric` in resolution.rs -> `pub(super)`
    - `toposort_facts`, `inline_facts`, `inline_derived_metrics`, `collect_derived_metric_using` in facts.rs -> `pub(super)`
    - `check_fan_traps` in fan_trap.rs -> `pub(super)`
    - `relationships_to_table`, `find_using_context` in role_playing.rs -> `pub(super)`
    - `synthesize_on_clause`, `synthesize_on_clause_scoped`, `resolve_joins_pkfk` in join_resolver.rs -> `pub(super)`

    **Do NOT modify any external consumer files** (query/explain.rs, query/table_function.rs, query/error.rs, ddl/show_dims_for_metric.rs). The mod.rs re-exports ensure all existing import paths continue to work.

    **Do NOT modify src/lib.rs**. The `pub mod expand;` declaration resolves to src/expand/mod.rs automatically.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo check 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - src/expand/mod.rs exists and contains `pub use types::{ExpandError, QueryRequest}`
    - src/expand/mod.rs contains `pub use resolution::{quote_ident, quote_table_ref}`
    - src/expand/mod.rs contains `pub use sql_gen::expand`
    - src/expand/mod.rs contains `pub(crate) use facts::collect_derived_metric_source_tables`
    - src/expand/mod.rs contains `pub(crate) use fan_trap::ancestors_to_root`
    - src/expand/types.rs contains `pub struct QueryRequest`
    - src/expand/types.rs contains `pub enum ExpandError`
    - src/expand/resolution.rs contains `pub fn quote_ident`
    - src/expand/resolution.rs contains `pub(super) fn find_dimension`
    - src/expand/facts.rs contains `pub(crate) fn collect_derived_metric_source_tables`
    - src/expand/facts.rs contains `pub(super) fn toposort_facts`
    - src/expand/fan_trap.rs contains `pub(crate) fn ancestors_to_root`
    - src/expand/fan_trap.rs contains `pub(super) fn check_fan_traps`
    - src/expand/role_playing.rs contains `pub(super) fn find_using_context`
    - src/expand/join_resolver.rs contains `pub(super) fn resolve_joins_pkfk`
    - src/expand/sql_gen.rs contains `pub fn expand(`
    - src/expand.rs does NOT exist (ls returns error)
    - `cargo check` exits 0 (no compilation errors)
  </acceptance_criteria>
  <done>All 7 production code submodules created with correct visibility, mod.rs re-exports the full prior public API, old expand.rs deleted, cargo check passes</done>
</task>

<task type="auto">
  <name>Task 2: Distribute expand tests and create test_helpers.rs</name>
  <files>
    src/expand/test_helpers.rs
    src/expand/resolution.rs
    src/expand/sql_gen.rs
    src/expand/facts.rs
    src/expand/fan_trap.rs
  </files>
  <read_first>
    src/expand.rs (read the #[cfg(test)] section starting at line 1367 to end-of-file -- this is the source of all test code)
    src/expand/mod.rs (verify test_helpers module declaration)
    src/expand/resolution.rs (will receive quote tests)
    src/expand/sql_gen.rs (will receive most expand tests)
  </read_first>
  <action>
    Distribute the ~2,932 lines of test code (lines 1367-4299 of the original expand.rs) into the appropriate submodule files, and create a shared test_helpers.rs module.

    NOTE: The original expand.rs was already deleted in Task 1. The test code must be copied from the git history or from the executor's context window (since Task 1 read the full file). The executor MUST have the test code available from the Task 1 read. If not, recover via `git show HEAD:src/expand.rs` to get the original content.

    **src/expand/test_helpers.rs** -- Shared test builder functions:
    - Extract the `make_def()` helper function from the test modules (it appears in the test section around line ~1433 onward in the original file, used by nearly all test modules)
    - The file is gated by `#[cfg(test)]` in mod.rs (already declared as `mod test_helpers;`)
    - Signature: `pub(super) fn make_def(tables: Vec<(&str, &str, Vec<&str>)>, joins: Vec<(&str, &str, Vec<&str>)>, dims: Vec<(&str, Option<&str>)>, metrics: Vec<(&str, Option<&str>)>) -> SemanticViewDefinition`
    - Also extract any other shared helpers used across multiple test modules (e.g., helper functions for building definitions with facts, derived metrics, etc.)
    - Import: `use crate::model::*;`

    **Test distribution** -- Add `#[cfg(test)] mod tests { ... }` blocks to submodules:

    1. **resolution.rs** gets: `quote_ident_tests` (~20 lines) and `quote_table_ref_tests` (~40 lines)
       - These test `quote_ident()` and `quote_table_ref()` directly
       - Tests use `use super::*;` which resolves to resolution module items

    2. **sql_gen.rs** gets ALL end-to-end expand tests (the vast majority):
       - `expand_tests` (~100 lines)
       - `phase11_1_expand_tests` (~100 lines)
       - `phase12_cast_tests` (~100 lines)
       - `phase26_pkfk_expand_tests` (~150 lines)
       - `phase27_qualified_refs_tests` (~145 lines)
       - `phase29_fact_inlining_tests` (~300 lines)
       - `phase30_derived_metric_tests` (~475 lines)
       - `phase31_fan_trap_tests` (~345 lines)
       - `phase32_role_playing_tests` (~485 lines)
       - These all call `expand()` end-to-end, so they belong with sql_gen
       - Tests need: `use super::super::test_helpers::make_def;` (or `use crate::expand::test_helpers::make_def;`)
       - Tests also need: `use crate::expand::{expand, QueryRequest, ExpandError};` and `use crate::model::*;`
       - For tests that build definitions with facts/derived metrics, they may have local helper functions -- keep those inline in the test module

    **Import adjustments in test blocks:**
    - Replace `use super::*;` patterns with explicit imports from `crate::expand::*` or `super::*` as appropriate
    - In sql_gen.rs tests: `use crate::expand::{expand, ExpandError, QueryRequest};` and `use super::super::test_helpers::make_def;`
    - In resolution.rs tests: `use super::{quote_ident, quote_table_ref};` (these are pub in the module)

    **Do NOT change any test logic or assertions.** This is a pure move operation. Every `#[test]` function body stays identical.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test --lib -- expand 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - src/expand/test_helpers.rs exists and contains `pub(super) fn make_def(`
    - src/expand/test_helpers.rs is gated by `#[cfg(test)]` (declared in mod.rs as `#[cfg(test)] mod test_helpers;`)
    - src/expand/resolution.rs contains `#[cfg(test)]` and `mod tests`
    - src/expand/resolution.rs tests contain `fn test_quote_ident` or equivalent quote_ident test
    - src/expand/sql_gen.rs contains `#[cfg(test)]`
    - src/expand/sql_gen.rs contains test functions from expand_tests, phase29, phase30, phase31, phase32 modules
    - `cargo test --lib -- expand` exits 0 with 86 tests passing (all original expand tests)
    - `cargo test` exits 0 (full unit test suite)
  </acceptance_criteria>
  <done>All 86 expand.rs tests distributed to their correct submodules, test_helpers.rs created with shared make_def helper, all tests pass with zero behavior changes</done>
</task>

</tasks>

<verification>
After both tasks complete:

1. `cargo check` passes (no compilation errors)
2. `cargo test --lib -- expand` passes all 86 expand tests
3. `cargo test` passes all 482+ tests
4. `ls src/expand.rs` returns "No such file or directory"
5. `ls src/expand/mod.rs src/expand/types.rs src/expand/resolution.rs src/expand/facts.rs src/expand/fan_trap.rs src/expand/role_playing.rs src/expand/join_resolver.rs src/expand/sql_gen.rs src/expand/test_helpers.rs` all exist
6. No external consumer files were modified (query/explain.rs, query/table_function.rs, query/error.rs, ddl/show_dims_for_metric.rs unchanged)
</verification>

<success_criteria>
- src/expand/ module directory exists with 7 submodules + mod.rs + test_helpers.rs
- src/expand.rs deleted (no E0761 ambiguity)
- mod.rs re-exports: ExpandError, QueryRequest, quote_ident, quote_table_ref, expand (pub), collect_derived_metric_source_tables, ancestors_to_root (pub(crate))
- All 86 expand tests pass in new locations
- cargo test passes with zero behavior changes
- No external import paths changed
</success_criteria>

<output>
After completion, create `.planning/phases/38-module-directory-splits/38-01-SUMMARY.md`
</output>
