---
phase: 37-extract-shared-utilities
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/lib.rs
  - src/util.rs
  - src/errors.rs
  - src/expand.rs
  - src/graph.rs
  - src/parse.rs
  - src/body_parser.rs
  - src/query/table_function.rs
  - src/query/explain.rs
  - src/ddl/show_dims_for_metric.rs
autonomous: true
requirements: [REF-03, REF-04]

must_haves:
  truths:
    - "suggest_closest is importable from crate::util by expand, graph, table_function, explain, and show_dims_for_metric"
    - "replace_word_boundary and is_word_boundary_char are importable from crate::util by expand"
    - "ParseError is importable from crate::errors by both parse and body_parser"
    - "graph.rs no longer imports anything from crate::expand (circular dependency broken)"
    - "body_parser.rs no longer imports anything from crate::parse (circular dependency broken)"
    - "All 482+ existing tests pass with zero behavior changes"
  artifacts:
    - path: "src/util.rs"
      provides: "suggest_closest, replace_word_boundary, is_word_boundary_char functions + 11 unit tests"
      contains: "pub fn suggest_closest"
    - path: "src/errors.rs"
      provides: "ParseError struct"
      contains: "pub struct ParseError"
  key_links:
    - from: "src/graph.rs"
      to: "src/util.rs"
      via: "use crate::util::suggest_closest"
      pattern: "use crate::util::suggest_closest"
    - from: "src/expand.rs"
      to: "src/util.rs"
      via: "use crate::util::{is_word_boundary_char, replace_word_boundary, suggest_closest}"
      pattern: "use crate::util::"
    - from: "src/body_parser.rs"
      to: "src/errors.rs"
      via: "use crate::errors::ParseError"
      pattern: "use crate::errors::ParseError"
    - from: "src/parse.rs"
      to: "src/errors.rs"
      via: "use crate::errors::ParseError"
      pattern: "use crate::errors::ParseError"
---

<objective>
Extract shared utility functions and error types into new leaf modules to break two circular module dependencies, preparing for the Phase 38 module directory splits.

Purpose: The expand<->graph and parse<->body_parser circular dependencies make the codebase harder to reason about and will complicate Phase 38's module directory splits. Breaking these now as a separate behavior-preserving step isolates risk.

Output: Two new leaf modules (src/util.rs, src/errors.rs) with zero behavior changes across all 482+ tests.
</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/37-extract-shared-utilities/37-RESEARCH.md

Source files being modified:
@src/lib.rs
@src/expand.rs (lines 1-30, 620-665 for functions to extract; lines 2767-2842 for tests to move)
@src/graph.rs (lines 1-15 for imports)
@src/parse.rs (lines 605-615 for ParseError definition)
@src/body_parser.rs (lines 1-8 for imports)
@src/query/table_function.rs (lines 1-15 for imports)
@src/query/explain.rs (lines 1-12 for imports)
@src/ddl/show_dims_for_metric.rs (lines 1-12 for imports)

<interfaces>
<!-- Current imports that must change. These are the exact lines the executor needs. -->

From src/expand.rs (line 1-29):
```rust
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use crate::graph::RelationshipGraph;
use crate::model::{Cardinality, Fact, Join, SemanticViewDefinition, TableRef};
// Lines 13-29: pub fn suggest_closest(...) -- MOVING TO util.rs
// Lines 629-658: fn replace_word_boundary(...) -- MOVING TO util.rs
// Lines 661-663: fn is_word_boundary_char(...) -- MOVING TO util.rs
```

From src/graph.rs (line 13):
```rust
use crate::expand::suggest_closest;
// CHANGES TO: use crate::util::suggest_closest;
```

From src/query/table_function.rs (line 13):
```rust
use crate::expand::{expand, suggest_closest, QueryRequest};
// CHANGES TO:
// use crate::expand::{expand, QueryRequest};
// use crate::util::suggest_closest;
```

From src/query/explain.rs (line 9):
```rust
use crate::expand::{expand, suggest_closest, QueryRequest};
// CHANGES TO:
// use crate::expand::{expand, QueryRequest};
// use crate::util::suggest_closest;
```

From src/ddl/show_dims_for_metric.rs (line 10):
```rust
use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables, suggest_closest};
// CHANGES TO:
// use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables};
// use crate::util::suggest_closest;
```

From src/parse.rs (lines 605-615):
```rust
#[derive(Debug)]
pub struct ParseError {
    pub message: String,
    pub position: Option<usize>,
}
// MOVING TO errors.rs
```

From src/body_parser.rs (line 7):
```rust
use crate::parse::ParseError;
// CHANGES TO: use crate::errors::ParseError;
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Extract util.rs leaf module (REF-03)</name>
  <files>src/util.rs, src/lib.rs, src/expand.rs, src/graph.rs, src/query/table_function.rs, src/query/explain.rs, src/ddl/show_dims_for_metric.rs</files>
  <read_first>
    - src/lib.rs (current module declarations -- add pub mod util before any existing mod)
    - src/expand.rs (lines 1-30 for suggest_closest; lines 620-665 for replace_word_boundary + is_word_boundary_char; lines 2767-2842 for 11 tests to move)
    - src/graph.rs (line 13 for current import)
    - src/query/table_function.rs (line 13 for current import)
    - src/query/explain.rs (line 9 for current import)
    - src/ddl/show_dims_for_metric.rs (line 10 for current import)
  </read_first>
  <action>
**Step 1: Create src/util.rs** with these exact contents:

```rust
//! Shared string utilities for fuzzy matching and word-boundary replacement.
//!
//! Extracted from `expand.rs` to break the expand <-> graph circular dependency.
//! Both `expand` and `graph` modules import from here.

/// Suggest the closest matching name from `available` using Levenshtein distance.
///
/// Returns `Some(name)` (with original casing) if the best match has an edit
/// distance of 3 or fewer characters. Returns `None` if no candidate is close
/// enough. Both the query and candidates are lowercased for comparison.
#[must_use]
pub fn suggest_closest(name: &str, available: &[String]) -> Option<String> {
    // EXACT copy from expand.rs lines 14-28 (the function body)
}

/// Replace all word-boundary occurrences of `needle` in `haystack` with `replacement`.
///
/// A word boundary is defined as: the character before the match (if any) is NOT
/// alphanumeric or underscore, AND the character after the match (if any) is NOT
/// alphanumeric or underscore. This prevents `net_price` from matching inside
/// `net_price_total` or `my_net_price`.
///
/// The matching is case-sensitive (fact names are identifiers).
pub fn replace_word_boundary(haystack: &str, needle: &str, replacement: &str) -> String {
    // EXACT copy from expand.rs lines 630-658 (the function body)
    // NOTE: change visibility from `fn` to `pub fn`
}

/// Check if a byte is a word-boundary character (NOT alphanumeric or underscore).
pub fn is_word_boundary_char(b: u8) -> bool {
    // EXACT copy from expand.rs lines 662-663 (the function body)
    // NOTE: change visibility from `fn` to `pub fn`
}

#[cfg(test)]
mod tests {
    use super::*;

    // Move all 11 replace_word_boundary tests from expand.rs
    // lines 2775-2841 (from replace_word_boundary_no_match through
    // replace_word_boundary_empty_needle). Copy them verbatim --
    // they call replace_word_boundary() directly via super::*.
}
```

**Step 2: Add module declaration to src/lib.rs.** Add these two lines BEFORE the existing `pub mod body_parser;` line (alphabetical order):

```rust
pub mod errors;
pub mod util;
```

The full module list should read:
```rust
pub mod body_parser;
pub mod catalog;
pub mod errors;
pub mod expand;
pub mod graph;
pub mod model;
pub mod parse;
pub mod util;
```

(Note: `errors` and `util` inserted alphabetically among the existing declarations. The `#[cfg(feature = "extension")]` blocks below remain unchanged.)

**Step 3: Modify src/expand.rs:**
- DELETE the `suggest_closest` function (lines 7-29, including the doc comment starting at line 7 through the closing brace at line 29)
- DELETE the `replace_word_boundary` function (lines 621-658 area, including doc comment)
- DELETE the `is_word_boundary_char` function (lines 661-663 area, including doc comment)
- ADD import at top of file (after existing `use` statements): `use crate::util::{is_word_boundary_char, replace_word_boundary, suggest_closest};`
- DELETE the 11 `replace_word_boundary_*` test functions from `mod phase29_fact_inlining_tests` (lines 2772-2841, from the `// replace_word_boundary tests` comment through `replace_word_boundary_empty_needle`). Keep the `// toposort_facts tests` section and everything after it.

**Step 4: Modify src/graph.rs line 13:**
- CHANGE: `use crate::expand::suggest_closest;` TO: `use crate::util::suggest_closest;`

**Step 5: Modify src/query/table_function.rs line 13:**
- CHANGE: `use crate::expand::{expand, suggest_closest, QueryRequest};`
- TO (two lines):
  ```rust
  use crate::expand::{expand, QueryRequest};
  use crate::util::suggest_closest;
  ```

**Step 6: Modify src/query/explain.rs line 9:**
- CHANGE: `use crate::expand::{expand, suggest_closest, QueryRequest};`
- TO (two lines):
  ```rust
  use crate::expand::{expand, QueryRequest};
  use crate::util::suggest_closest;
  ```

**Step 7: Modify src/ddl/show_dims_for_metric.rs line 10:**
- CHANGE: `use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables, suggest_closest};`
- TO (two lines):
  ```rust
  use crate::expand::{ancestors_to_root, collect_derived_metric_source_tables};
  use crate::util::suggest_closest;
  ```

**Step 8: Verify compilation:** `cargo test` must pass. All 482+ tests must remain green.
  </action>
  <verify>
    <automated>cargo test 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - src/util.rs exists and contains `pub fn suggest_closest(name: &str, available: &[String]) -> Option<String>`
    - src/util.rs contains `pub fn replace_word_boundary(haystack: &str, needle: &str, replacement: &str) -> String`
    - src/util.rs contains `pub fn is_word_boundary_char(b: u8) -> bool`
    - src/util.rs contains `#[cfg(test)]` and `mod tests` with 11 test functions (replace_word_boundary_no_match through replace_word_boundary_empty_needle)
    - src/lib.rs contains `pub mod util;`
    - src/expand.rs does NOT contain `pub fn suggest_closest` (function definition removed)
    - src/expand.rs does NOT contain `fn replace_word_boundary` (function definition removed)
    - src/expand.rs does NOT contain `fn is_word_boundary_char` (function definition removed)
    - src/expand.rs contains `use crate::util::{is_word_boundary_char, replace_word_boundary, suggest_closest};`
    - src/graph.rs contains `use crate::util::suggest_closest;` (NOT `use crate::expand::suggest_closest`)
    - src/query/table_function.rs contains `use crate::util::suggest_closest;`
    - src/query/explain.rs contains `use crate::util::suggest_closest;`
    - src/ddl/show_dims_for_metric.rs contains `use crate::util::suggest_closest;`
    - `cargo test` exits 0 with all tests passing
  </acceptance_criteria>
  <done>suggest_closest, replace_word_boundary, and is_word_boundary_char live in src/util.rs. graph.rs imports suggest_closest from crate::util (not crate::expand). All 4 external import sites updated. 11 replace_word_boundary tests moved to util::tests. cargo test passes.</done>
</task>

<task type="auto">
  <name>Task 2: Extract errors.rs leaf module and run full test suite (REF-04)</name>
  <files>src/errors.rs, src/parse.rs, src/body_parser.rs</files>
  <read_first>
    - src/parse.rs (lines 605-615 for ParseError struct definition to extract)
    - src/body_parser.rs (line 7 for current import path)
    - src/lib.rs (confirm pub mod errors; was added in Task 1)
  </read_first>
  <action>
**Step 1: Create src/errors.rs** with these exact contents:

```rust
//! Shared error types for the semantic views parser pipeline.
//!
//! Extracted from `parse.rs` to break the parse <-> body_parser circular dependency.
//! Both `parse` and `body_parser` modules import from here.

/// Error from DDL validation with an optional byte offset into the original query.
///
/// The `position` field, when present, is a 0-based byte offset into the
/// original query string (before any trimming). `DuckDB` uses this to render
/// a caret (`^`) under the error location.
#[derive(Debug)]
pub struct ParseError {
    pub message: String,
    /// Byte offset into the original query string.
    pub position: Option<usize>,
}
```

(Note: `pub mod errors;` was already added to src/lib.rs in Task 1.)

**Step 2: Modify src/parse.rs:**
- DELETE the `ParseError` struct definition (lines 605-615, including the doc comment starting at line 605 `/// Error from DDL validation...` through the closing brace at line 615)
- ADD import at top of file (after the existing `use` statements, before `pub const PARSE_NOT_OURS`): `use crate::errors::ParseError;`

**Step 3: Modify src/body_parser.rs line 7:**
- CHANGE: `use crate::parse::ParseError;`
- TO: `use crate::errors::ParseError;`

**Step 4: Run the full quality gate:**
```bash
just test-all
```

This must pass: Rust unit tests, proptests, sqllogictests, and DuckLake CI tests. A phase that only runs `cargo test` is incomplete per CLAUDE.md.
  </action>
  <verify>
    <automated>just test-all 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - src/errors.rs exists and contains `pub struct ParseError`
    - src/errors.rs contains `pub message: String` and `pub position: Option<usize>`
    - src/lib.rs contains `pub mod errors;`
    - src/parse.rs does NOT contain `pub struct ParseError` (struct definition removed)
    - src/parse.rs contains `use crate::errors::ParseError;`
    - src/body_parser.rs contains `use crate::errors::ParseError;` (NOT `use crate::parse::ParseError`)
    - `just test-all` exits 0 with all tests passing (Rust unit, proptest, sqllogictest, DuckLake CI)
  </acceptance_criteria>
  <done>ParseError lives in src/errors.rs. Both parse.rs and body_parser.rs import from crate::errors. The parse<->body_parser circular dependency is broken. `just test-all` passes with zero behavior changes across all 482+ tests.</done>
</task>

</tasks>

<verification>
Phase 37 is verified when:

1. **Structural verification** (compile-time):
   - `cargo check` passes (proves all import paths resolve)
   - `src/util.rs` exists as a leaf module (no `use crate::` imports except `strsim`)
   - `src/errors.rs` exists as a leaf module (no `use crate::` imports)
   - `src/graph.rs` has zero imports from `crate::expand` (circular dependency broken)
   - `src/body_parser.rs` has zero imports from `crate::parse` except through `crate::errors` (circular dependency broken)

2. **Behavioral verification** (runtime):
   - `just test-all` passes (all Rust unit tests, proptests, sqllogictests, DuckLake CI)
   - Test count is unchanged (482+ tests)

3. **Negative checks:**
   - `grep -r "use crate::expand::suggest_closest" src/` returns zero matches (all moved to crate::util)
   - `grep "pub fn suggest_closest" src/expand.rs` returns zero matches (definition removed)
   - `grep "pub struct ParseError" src/parse.rs` returns zero matches (definition removed)
</verification>

<success_criteria>
- src/util.rs exists with suggest_closest, replace_word_boundary, is_word_boundary_char (all pub fn) and 11 unit tests
- src/errors.rs exists with ParseError struct
- graph.rs imports suggest_closest from crate::util (not crate::expand)
- body_parser.rs imports ParseError from crate::errors (not crate::parse)
- No function signature changes -- identical public API surface
- `just test-all` green with all 482+ tests passing
</success_criteria>

<output>
After completion, create `.planning/phases/37-extract-shared-utilities/37-01-SUMMARY.md`
</output>
