---
phase: 01-symbol-disambiguation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/live_index/query.rs
autonomous: true
requirements:
  - SYMB-01
  - SYMB-02
  - SYMB-03

must_haves:
  truths:
    - "Looking up a C# class by name returns the class definition even when a same-named constructor exists"
    - "resolve_symbol_selector applies kind-tier priority to auto-disambiguate cross-kind matches"
    - "Two symbols at the same priority tier still produce an Ambiguous error with candidates listed"
    - "Existing single-match and not-found behavior is unchanged"
  artifacts:
    - path: "src/live_index/query.rs"
      provides: "kind_disambiguation_tier function and updated resolve_symbol_selector"
      contains: "fn kind_disambiguation_tier"
  key_links:
    - from: "resolve_symbol_selector"
      to: "kind_disambiguation_tier"
      via: "called in multi-candidate branch to assign tiers and auto-select highest"
      pattern: "kind_disambiguation_tier"
---

<objective>
Replace the binary container-vs-member heuristic in `resolve_symbol_selector` with a proper kind-tier priority system that auto-disambiguates when candidates span different kind tiers, while preserving the Ambiguous error for same-tier collisions.

Purpose: C# class/constructor name collisions (and similar cross-kind ambiguities in Java, Kotlin, etc.) currently require the user to pass `symbol_kind` or `symbol_line` to disambiguate. This fix makes the common case (looking up a class by name) work automatically.

Output: Updated `resolve_symbol_selector` with tier-based disambiguation and comprehensive tests.
</objective>

<execution_context>
@E:/project/symforge/.claude/get-shit-done/workflows/execute-plan.md
@E:/project/symforge/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-symbol-disambiguation/01-CONTEXT.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From src/live_index/query.rs:
```rust
pub(crate) enum SymbolSelectorMatch<'a> {
    Selected(usize, &'a SymbolRecord),
    NotFound,
    Ambiguous(Vec<u32>),
}

pub(crate) fn resolve_symbol_selector<'a>(
    file: &'a IndexedFile,
    name: &str,
    symbol_kind: Option<&str>,
    symbol_line: Option<u32>,
) -> SymbolSelectorMatch<'a>;
```

From src/domain/index.rs:
```rust
pub enum SymbolKind {
    Function, Method, Class, Struct, Enum, Interface,
    Module, Constant, Variable, Type, Trait, Impl,
    Other, Key, Section,
}
```

From src/parsing/languages/csharp.rs (line 31):
```rust
"constructor_declaration" => Some(SymbolKind::Function),
```
Note: C# constructors are mapped to SymbolKind::Function, NOT a separate Constructor kind.

Test helpers in query.rs (line 2776+):
```rust
fn make_symbol(name: &str) -> SymbolRecord  // kind defaults to Function, line_range (0,1)
fn make_indexed_file(path: &str, symbols: Vec<SymbolRecord>, status: ParseStatus) -> IndexedFile
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add kind_disambiguation_tier function and tests</name>
  <files>src/live_index/query.rs</files>
  <read_first>
    - src/live_index/query.rs (lines 377-450 for resolve_symbol_selector, lines 2761-2810 for test helpers)
    - src/domain/index.rs (lines 354-370 for SymbolKind enum)
  </read_first>
  <behavior>
    - Test: kind_disambiguation_tier returns tier 1 for Class, Struct, Enum, Interface, Trait
    - Test: kind_disambiguation_tier returns tier 2 for Module
    - Test: kind_disambiguation_tier returns tier 3 for Function, Method, Impl
    - Test: kind_disambiguation_tier returns tier 4 for Constant, Variable, Type, Key, Section, Other
    - Test: resolve_symbol_selector with a Class "Foo" (line 0) and a Function "Foo" (line 10) returns Selected for the Class (SYMB-02 scenario)
    - Test: resolve_symbol_selector with a Module "Foo" (line 0) and a Function "Foo" (line 10) returns Selected for the Module (cross-tier, tier 2 > tier 3)
    - Test: resolve_symbol_selector with two Function "connect" symbols at different lines returns Ambiguous with both lines (SYMB-03 scenario)
    - Test: resolve_symbol_selector with a Class "Foo" and a Struct "Foo" returns Ambiguous (same tier 1, SYMB-03 scenario)
    - Test: resolve_symbol_selector with explicit symbol_kind="fn" still filters correctly (no tier logic when kind specified)
    - Test: three-way disambiguation: Class "Foo" + Module "Foo" + Function "Foo" returns the Class (highest tier wins)
  </behavior>
  <action>
Add a `kind_disambiguation_tier` function and update `resolve_symbol_selector` in `src/live_index/query.rs`.

**Step 1: Add the tier function** (place it just above `resolve_symbol_selector`, around line 383):

```rust
/// Returns a disambiguation tier for a SymbolKind.  Lower number = higher
/// priority.  Used by `resolve_symbol_selector` to auto-pick the most
/// likely intended symbol when multiple candidates share the same name but
/// differ in kind.
///
/// Tier 1 — type definitions (class, struct, enum, interface, trait)
/// Tier 2 — namespace containers (module)
/// Tier 3 — callables (function, method, impl)
/// Tier 4 — everything else (constant, variable, type alias, key, section, other)
fn kind_disambiguation_tier(kind: &SymbolKind) -> u8 {
    match kind {
        SymbolKind::Class
        | SymbolKind::Struct
        | SymbolKind::Enum
        | SymbolKind::Interface
        | SymbolKind::Trait => 1,
        SymbolKind::Module => 2,
        SymbolKind::Function | SymbolKind::Method | SymbolKind::Impl => 3,
        _ => 4, // Constant, Variable, Type, Key, Section, Other
    }
}
```

**Step 2: Replace the container-vs-member heuristic** in `resolve_symbol_selector` (the `_ =>` match arm, lines 411-448). Replace the existing `if symbol_kind.is_none() { ... container_indices ... }` block with:

```rust
        _ => {
            // Kind-tier disambiguation: when the user did NOT specify
            // symbol_kind, group candidates by kind tier and auto-select
            // if the highest tier has exactly one candidate.  This resolves
            // the common C#/Java/Kotlin pattern where a class and its
            // constructor share the same name (class=tier 1, constructor
            // mapped to Function=tier 3).
            if symbol_kind.is_none() {
                let min_tier = candidates
                    .iter()
                    .map(|(_, sym)| kind_disambiguation_tier(&sym.kind))
                    .min()
                    .unwrap(); // safe: candidates.len() >= 2

                let top_tier: Vec<usize> = candidates
                    .iter()
                    .enumerate()
                    .filter(|(_, (_, sym))| kind_disambiguation_tier(&sym.kind) == min_tier)
                    .map(|(i, _)| i)
                    .collect();

                if top_tier.len() == 1 {
                    let (idx, symbol) = candidates.remove(top_tier[0]);
                    return SymbolSelectorMatch::Selected(idx, symbol);
                }
            }

            SymbolSelectorMatch::Ambiguous(
                candidates
                    .into_iter()
                    .map(|(_, symbol)| symbol.line_range.0)
                    .collect(),
            )
        }
```

**Step 3: Add tests** at the end of the `mod tests` block (before the final closing `}`). Use the existing `make_symbol` helper, overriding `kind` and `line_range` fields as needed:

```rust
    // -----------------------------------------------------------------------
    // kind_disambiguation_tier + resolve_symbol_selector tier tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_kind_disambiguation_tier_values() {
        use super::kind_disambiguation_tier;
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Class), 1);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Struct), 1);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Enum), 1);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Interface), 1);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Trait), 1);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Module), 2);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Function), 3);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Method), 3);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Impl), 3);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Constant), 4);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Variable), 4);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Type), 4);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Key), 4);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Section), 4);
        assert_eq!(kind_disambiguation_tier(&SymbolKind::Other), 4);
    }

    #[test]
    fn test_resolve_selector_class_vs_constructor_returns_class() {
        // SYMB-02: C# class "Foo" + constructor "Foo" (mapped to Function)
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let class_sym = SymbolRecord {
            kind: SymbolKind::Class,
            line_range: (0, 20),
            ..make_symbol("Foo")
        };
        let ctor_sym = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (5, 15),
            ..make_symbol("Foo")
        };
        let file = make_indexed_file("src/Foo.cs", vec![class_sym, ctor_sym], ParseStatus::Full);
        match resolve_symbol_selector(&file, "Foo", None, None) {
            SymbolSelectorMatch::Selected(idx, sym) => {
                assert_eq!(idx, 0);
                assert_eq!(sym.kind, SymbolKind::Class);
            }
            other => panic!("Expected Selected, got {:?}", match other {
                SymbolSelectorMatch::Ambiguous(v) => format!("Ambiguous({v:?})"),
                SymbolSelectorMatch::NotFound => "NotFound".to_string(),
                _ => unreachable!(),
            }),
        }
    }

    #[test]
    fn test_resolve_selector_module_vs_function_returns_module() {
        // Cross-tier: module (tier 2) beats function (tier 3)
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let mod_sym = SymbolRecord {
            kind: SymbolKind::Module,
            line_range: (0, 50),
            ..make_symbol("utils")
        };
        let fn_sym = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (10, 20),
            ..make_symbol("utils")
        };
        let file = make_indexed_file("src/utils.py", vec![mod_sym, fn_sym], ParseStatus::Full);
        match resolve_symbol_selector(&file, "utils", None, None) {
            SymbolSelectorMatch::Selected(idx, sym) => {
                assert_eq!(idx, 0);
                assert_eq!(sym.kind, SymbolKind::Module);
            }
            other => panic!("Expected Selected for Module, got {:?}", match other {
                SymbolSelectorMatch::Ambiguous(v) => format!("Ambiguous({v:?})"),
                SymbolSelectorMatch::NotFound => "NotFound".to_string(),
                _ => unreachable!(),
            }),
        }
    }

    #[test]
    fn test_resolve_selector_same_tier_returns_ambiguous() {
        // SYMB-03: Two functions with same name = same tier = Ambiguous
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let fn1 = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (0, 5),
            ..make_symbol("connect")
        };
        let fn2 = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (10, 15),
            ..make_symbol("connect")
        };
        let file = make_indexed_file("src/db.rs", vec![fn1, fn2], ParseStatus::Full);
        match resolve_symbol_selector(&file, "connect", None, None) {
            SymbolSelectorMatch::Ambiguous(lines) => {
                assert_eq!(lines, vec![0, 10]);
            }
            _ => panic!("Expected Ambiguous for same-tier symbols"),
        }
    }

    #[test]
    fn test_resolve_selector_same_tier_class_struct_returns_ambiguous() {
        // SYMB-03: Class + Struct at tier 1 = Ambiguous
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let class_sym = SymbolRecord {
            kind: SymbolKind::Class,
            line_range: (0, 10),
            ..make_symbol("Data")
        };
        let struct_sym = SymbolRecord {
            kind: SymbolKind::Struct,
            line_range: (20, 30),
            ..make_symbol("Data")
        };
        let file = make_indexed_file("src/data.rs", vec![class_sym, struct_sym], ParseStatus::Full);
        match resolve_symbol_selector(&file, "Data", None, None) {
            SymbolSelectorMatch::Ambiguous(lines) => {
                assert_eq!(lines, vec![0, 20]);
            }
            _ => panic!("Expected Ambiguous for two tier-1 symbols"),
        }
    }

    #[test]
    fn test_resolve_selector_explicit_kind_bypasses_tier_logic() {
        // When symbol_kind is specified, tier logic should NOT apply
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let class_sym = SymbolRecord {
            kind: SymbolKind::Class,
            line_range: (0, 20),
            ..make_symbol("Foo")
        };
        let fn_sym = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (5, 15),
            ..make_symbol("Foo")
        };
        let file = make_indexed_file("src/Foo.cs", vec![class_sym, fn_sym], ParseStatus::Full);
        // Asking specifically for "fn" should return the function, not the class
        match resolve_symbol_selector(&file, "Foo", Some("fn"), None) {
            SymbolSelectorMatch::Selected(idx, sym) => {
                assert_eq!(idx, 1);
                assert_eq!(sym.kind, SymbolKind::Function);
            }
            _ => panic!("Expected Selected for explicit kind filter"),
        }
    }

    #[test]
    fn test_resolve_selector_three_way_picks_highest_tier() {
        // Class (tier 1) + Module (tier 2) + Function (tier 3) => Class wins
        use super::{resolve_symbol_selector, SymbolSelectorMatch};
        let fn_sym = SymbolRecord {
            kind: SymbolKind::Function,
            line_range: (30, 40),
            ..make_symbol("Foo")
        };
        let mod_sym = SymbolRecord {
            kind: SymbolKind::Module,
            line_range: (0, 50),
            ..make_symbol("Foo")
        };
        let class_sym = SymbolRecord {
            kind: SymbolKind::Class,
            line_range: (10, 25),
            ..make_symbol("Foo")
        };
        // Intentionally not in tier order to test sorting
        let file = make_indexed_file("src/Foo.cs", vec![fn_sym, mod_sym, class_sym], ParseStatus::Full);
        match resolve_symbol_selector(&file, "Foo", None, None) {
            SymbolSelectorMatch::Selected(idx, sym) => {
                assert_eq!(idx, 2); // class_sym is at index 2 in the vec
                assert_eq!(sym.kind, SymbolKind::Class);
            }
            other => panic!("Expected Selected for Class in 3-way, got {:?}", match other {
                SymbolSelectorMatch::Ambiguous(v) => format!("Ambiguous({v:?})"),
                SymbolSelectorMatch::NotFound => "NotFound".to_string(),
                _ => unreachable!(),
            }),
        }
    }
```

Note: The `make_symbol` helper returns `SymbolKind::Function` and `line_range: (0, 1)` by default. Override with struct update syntax `..make_symbol("name")` for each test case.
  </action>
  <verify>
    <automated>cd E:/project/symforge && cargo test --lib -- query::tests::test_kind_disambiguation_tier_values query::tests::test_resolve_selector_class_vs_constructor_returns_class query::tests::test_resolve_selector_module_vs_function_returns_module query::tests::test_resolve_selector_same_tier_returns_ambiguous query::tests::test_resolve_selector_same_tier_class_struct_returns_ambiguous query::tests::test_resolve_selector_explicit_kind_bypasses_tier_logic query::tests::test_resolve_selector_three_way_picks_highest_tier --test-threads=1</automated>
  </verify>
  <acceptance_criteria>
    - grep -c "fn kind_disambiguation_tier" src/live_index/query.rs returns 1
    - grep -c "min_tier" src/live_index/query.rs returns at least 1 (in resolve_symbol_selector)
    - grep -c "top_tier" src/live_index/query.rs returns at least 1 (in resolve_symbol_selector)
    - grep -c "container_indices" src/live_index/query.rs returns 0 (old heuristic removed)
    - grep -c "test_kind_disambiguation_tier_values" src/live_index/query.rs returns 1
    - grep -c "test_resolve_selector_class_vs_constructor_returns_class" src/live_index/query.rs returns 1
    - grep -c "test_resolve_selector_same_tier_returns_ambiguous" src/live_index/query.rs returns 1
    - grep -c "test_resolve_selector_same_tier_class_struct_returns_ambiguous" src/live_index/query.rs returns 1
    - grep -c "test_resolve_selector_three_way_picks_highest_tier" src/live_index/query.rs returns 1
    - cargo test --lib -- query::tests::test_kind_disambiguation query::tests::test_resolve_selector --test-threads=1 exits 0
  </acceptance_criteria>
  <done>
    - kind_disambiguation_tier function exists with 4 tiers covering all 15 SymbolKind variants
    - resolve_symbol_selector uses tier-based disambiguation instead of binary container heuristic
    - All 7 new tests pass
    - Old container_indices logic fully replaced (not just extended)
  </done>
</task>

<task type="auto">
  <name>Task 2: Verify full test suite and formatting</name>
  <files>src/live_index/query.rs</files>
  <read_first>
    - src/live_index/query.rs (lines 383-450 for the updated function)
  </read_first>
  <action>
Run the full test suite and rustfmt check to confirm no regressions were introduced.

1. Run `cargo fmt -- --check` and fix any formatting issues.
2. Run `cargo test --all-targets -- --test-threads=1` to verify the entire test suite passes.
3. Run `cargo check` for compilation verification.

If any existing tests fail, diagnose whether the tier-based heuristic changed behavior for a test case that relied on the old container-vs-member heuristic. The new tier system is a strict superset of the old heuristic (containers are all tier 1-2, members are tier 3-4), so no behavior change is expected for cases the old heuristic handled. If a test fails, inspect whether it expected `Ambiguous` for a case that the new tier logic now resolves (e.g., a Module + Function collision that the old code returned Ambiguous for because container_indices had 1 Module but the test expected Ambiguous). Fix by updating the test expectation if the new behavior is correct per the requirements.
  </action>
  <verify>
    <automated>cd E:/project/symforge && cargo fmt -- --check && cargo test --all-targets -- --test-threads=1</automated>
  </verify>
  <acceptance_criteria>
    - cargo fmt -- --check exits 0 (no formatting issues)
    - cargo test --all-targets -- --test-threads=1 exits 0 (all tests pass including existing ones)
    - cargo check exits 0 (no compilation warnings relevant to changed code)
  </acceptance_criteria>
  <done>
    - Full test suite green (0 failures)
    - Code formatted per project standards
    - No regressions in any existing resolve_symbol_selector call sites
  </done>
</task>

</tasks>

<verification>
Phase-level verification after both tasks complete:

1. `cargo test --all-targets -- --test-threads=1` passes (full suite)
2. `cargo fmt -- --check` passes
3. `grep "fn kind_disambiguation_tier" src/live_index/query.rs` confirms the tier function exists
4. `grep "container_indices" src/live_index/query.rs` returns nothing (old heuristic fully replaced)
5. The 7 new tests cover all 3 requirements:
   - SYMB-01: test_kind_disambiguation_tier_values, test_resolve_selector_module_vs_function_returns_module, test_resolve_selector_three_way_picks_highest_tier
   - SYMB-02: test_resolve_selector_class_vs_constructor_returns_class
   - SYMB-03: test_resolve_selector_same_tier_returns_ambiguous, test_resolve_selector_same_tier_class_struct_returns_ambiguous
</verification>

<success_criteria>
- resolve_symbol_selector auto-disambiguates when candidates span different kind tiers
- C# class lookup with same-named constructor returns the class without error
- Two symbols at the same tier still produce Ambiguous with candidate lines
- All existing tests pass with no regressions
- Code passes cargo fmt check
</success_criteria>

<output>
After completion, create `.planning/phases/01-symbol-disambiguation/01-01-SUMMARY.md`
</output>
