Now I have a complete picture. Let me design the plan.

---

## Implementation Plan: Consolidate Query-Building Logic

### Summary of Differences Found

After thorough analysis, here are all the concrete differences between the two implementations:

**Structural differences:**
1. `buildScopeQueries()` -- exists as a standalone function in `metricstoreclient`, inline in `metricstore`
2. Scope dedup: `map[schema.MetricScope]bool` (metricstore) vs labeled-loop + `[]schema.MetricScope` (metricstoreclient)
3. Topology access: inline `archive.GetSubCluster()` (metricstore) vs `getTopology()`/`getTopologyByNode()` with caching (metricstoreclient)

**Functional differences:**
4. **MemoryDomain -> Socket** transformation: present in `metricstoreclient/buildScopeQueries()` (lines 449-472), **missing** from `metricstore/buildQueries()` and `metricstore/buildNodeQueries()`
5. `sanitizeStats` signature: `(data *APIMetricData)` in metricstore vs `(avg, min, max *schema.Float)` in metricstoreclient. The metricstoreclient version zeros ALL three if ANY is NaN; the metricstore version zeros each independently.

**Type differences (not shareable without unification):**
6. `APIQuery.Resolution`: `int64` (metricstore) vs `int` (metricstoreclient)
7. `APIMetricData.Resolution`: `int64` vs `int`
8. `APIMetricData.Data`: `schema.FloatArray` vs `[]schema.Float`
9. `APIQuery.ScaleFactor`: present in metricstore, absent in metricstoreclient

### Design Decisions

Given the constraints (separate `APIQuery` types, different resolution types, different data fetching), the plan focuses on what **can** be shared without large-scale type unification:

**Share: `buildScopeQueries` logic** -- This is the highest-value target. It is pure scope-transformation logic that depends only on `schema.Topology`, `schema.MetricScope`, and string/int slices. It does NOT depend on `APIQuery` at all internally -- it only constructs the output fields.

**Approach: Generic scope-query builder returning intermediate structs.** Create a shared package-level function that returns a simple intermediate type (not `APIQuery`), which each package then converts to its own `APIQuery`. This avoids coupling to either `APIQuery` type.

**Alternative considered and rejected:** Making `buildScopeQueries` return `metricstoreclient.APIQuery` and importing it from `metricstore` -- this creates a wrong dependency direction (`pkg/` importing `internal/`). The reverse direction (moving to `pkg/`) is possible but `metricstoreclient` is an `internal/` package.

**Best location for shared code:** A new file in `pkg/metricstore/` (e.g., `pkg/metricstore/scopequery.go`), since `metricstoreclient` already imports `pkg/metricstore`. Both packages can access code there.

### Step-by-Step Plan

#### Step 1: Define shared intermediate type and function in `pkg/metricstore/scopequery.go`

Create a new file `pkg/metricstore/scopequery.go` containing:

```go
package metricstore

import "github.com/ClusterCockpit/cc-lib/v2/schema"

// ScopeQueryResult holds the fields needed to construct an APIQuery for a single scope query.
// This is a package-independent intermediate that both metricstore and metricstoreclient
// can convert to their respective APIQuery types.
type ScopeQueryResult struct {
    Metric    string
    Hostname  string
    Aggregate bool
    Type      *string            // nil for node-scope queries
    TypeIds   []string
    Scope     schema.MetricScope // The assigned (actual) scope
}

// BuildScopeQueries generates scope query descriptors for a given scope transformation.
// It returns (results, nil) on success, (empty, nil) for expected exceptions,
// and (nil, error) for unhandled cases.
func BuildScopeQueries(
    nativeScope, requestedScope schema.MetricScope,
    metric, hostname string,
    topology *schema.Topology,
    hwthreads []int,
    accelerators []string,
) ([]ScopeQueryResult, error) {
    // ... (port from metricstoreclient/buildScopeQueries,
    //      omitting Resolution since callers set it)
}
```

Key design points:
- `Resolution` is omitted from `ScopeQueryResult` because it differs in type (`int` vs `int64`) and callers always know it.
- The function is exported so both packages can use it.
- Move the scope string constants (`hwthreadString`, etc.) here as well -- both packages have identical copies.
- Move `intToStringSlice` here -- both packages have copies (the metricstore version is slightly more optimized with buffer reuse, use that).
- Include the **MemoryDomain -> Socket** transformation from metricstoreclient.

#### Step 2: Update `pkg/metricstore/query.go`

Refactor `buildQueries()` and `buildNodeQueries()`:
- Replace the inline scope-transformation if-chain (lines 317-534 in `buildQueries`, lines 1099-1314 in `buildNodeQueries`) with a call to `BuildScopeQueries()`.
- Convert each `ScopeQueryResult` to the local `APIQuery` by adding `Resolution: resolution`.
- Handle the return value convention: empty slice = expected exception (continue), nil = error.
- Remove the duplicated scope string constants and `intToStringSlice` (now in `scopequery.go`).
- Switch dedup from `map[schema.MetricScope]bool` to the labeled-loop + slice pattern used in metricstoreclient (more consistent, avoids allocation for the typical 1-3 scopes).

This change also **adds the missing MemoryDomain -> Socket transformation** to the internal store.

#### Step 3: Update `internal/metricstoreclient/cc-metric-store-queries.go`

Refactor `buildQueries()` and `buildNodeQueries()`:
- Replace call to local `buildScopeQueries()` with `metricstore.BuildScopeQueries()`.
- Convert each `ScopeQueryResult` to the local `APIQuery` by adding `Resolution: resolution`.
- Delete the local `buildScopeQueries()` function.
- Delete the local scope string constants and `intToStringSlice` (now in `pkg/metricstore/scopequery.go`).

#### Step 4: Synchronize `sanitizeStats`

Two options:

**Option A (recommended -- minimal change):** Align the metricstore `sanitizeStats` to match the metricstoreclient behavior (zero ALL if ANY is NaN). This is the safer behavior since partial NaN stats are likely corrupt. Change `pkg/metricstore/query.go` `sanitizeStats` to:
```go
func sanitizeStats(data *APIMetricData) {
    if data.Avg.IsNaN() || data.Min.IsNaN() || data.Max.IsNaN() {
        data.Avg = schema.Float(0)
        data.Min = schema.Float(0)
        data.Max = schema.Float(0)
    }
}
```

Do NOT share the function itself since the signatures differ due to different `APIMetricData` types. Just synchronize the logic.

**Option B:** Extract a shared `SanitizeStats(avg, min, max *schema.Float)` into `pkg/metricstore/scopequery.go` and have both call it. The metricstore version would wrap it:
```go
func sanitizeStats(data *APIMetricData) {
    metricstore.SanitizeStats(&data.Avg, &data.Min, &data.Max)
}
```
This is cleaner but adds an extra function call. Low cost, reasonable.

I recommend **Option B** for consistency.

#### Step 5: Add `hasNaNStats` to shared code (optional)

Move `hasNaNStats` to `pkg/metricstore/scopequery.go` as an exported function. Currently only used in `metricstoreclient/LoadStats`, but `metricstore/LoadStats` has the same check inline (line 602). Both can use the shared version.

#### Step 6: Add tests for `BuildScopeQueries`

Create `pkg/metricstore/scopequery_test.go` with table-driven tests covering all scope transformation cases, including the MemoryDomain -> Socket case. This is currently untested in both packages.

### What NOT to share (and why)

- **`buildQueries` / `buildNodeQueries` themselves**: These differ in topology access patterns (caching vs inline), empty-resource checks, and are methods on different receiver types. The shared `BuildScopeQueries` extracts the core logic while letting each package handle its own orchestration.
- **`APIQuery` / `APIMetricData` types**: Different `Resolution` types (`int` vs `int64`), different `Data` types (`schema.FloatArray` vs `[]schema.Float`), and `ScaleFactor` only in internal. Unifying would require touching too much code across both packages for marginal benefit.
- **Load* functions**: They share the same result-processing pattern but depend on package-specific types (`APIQuery`, `APIMetricData`) and fetching mechanisms (`FetchData` vs `doRequest`). Extracting generic result processors would require generics or interfaces that add complexity without proportional benefit.

### Migration sequence

1. Create `pkg/metricstore/scopequery.go` with `ScopeQueryResult`, `BuildScopeQueries`, `IntToStringSlice`, `SanitizeStats`, and scope string constants.
2. Add `pkg/metricstore/scopequery_test.go`.
3. Update `pkg/metricstore/query.go` to use the shared functions. Run `go test ./pkg/metricstore/...`.
4. Update `internal/metricstoreclient/cc-metric-store-queries.go` to use the shared functions. Run `go test ./internal/metricstoreclient/...` (currently no tests, but compile check).
5. Run full test suite: `make test`.

### Risks and mitigations

- **MemoryDomain -> Socket addition to internal store**: This is a functional change. The internal store previously returned an error for this case. The metricstoreclient handles it correctly. Adding it is a bug fix, not a regression.
- **sanitizeStats behavior change**: Changing from "zero each independently" to "zero all if any is NaN" could change output for edge cases where only one stat is NaN. This is arguably more correct (partial NaN stats are suspect). Low risk.
- **Import cycle risk**: None. `internal/metricstoreclient` already imports `pkg/metricstore`. The shared code goes into `pkg/metricstore`, which does not import `internal/metricstoreclient`.

### Critical Files for Implementation
- `/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/query.go` - Contains the inline scope transformation to replace with shared BuildScopeQueries calls; also buildQueries and buildNodeQueries to refactor
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricstoreclient/cc-metric-store-queries.go` - Contains the reference buildScopeQueries implementation to extract; buildQueries and buildNodeQueries to refactor
- `/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/api.go` - Defines the internal APIQuery/APIMetricData types; context for type differences
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricstoreclient/cc-metric-store.go` - Defines external APIQuery/APIMetricData types and Load* functions with result processing to synchronize
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/metricdata.go` - The MetricDataRepository interface both packages implement; must not be broken

agentId: aeb62a7c5fea57dd2 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 65135
tool_uses: 15
duration_ms: 102215</usage>

Prompt:
I need to design a plan to synchronize and consolidate the query-building logic between two packages that implement the same MetricDataRepository interface:

## Context

1. **`pkg/metricstore/query.go`** (InternalMetricStore) — in-memory metric store
2. **`internal/metricstoreclient/cc-metric-store-queries.go`** (CCMetricStore) — HTTP client to external cc-metric-store

Both implement the `MetricDataRepository` interface from `internal/metricdispatch/metricdata.go`.

## Key Differences Found

### buildQueries differences:
- **pkg/metricstore**: Inline scope transformation (long chain of if-statements in the inner loop). Uses `map[schema.MetricScope]bool` for dedup. Resolution is `int64`.
- **metricstoreclient**: Extracted `buildScopeQueries()` helper function. Uses labeled loop + `[]schema.MetricScope` slice for dedup. Resolution is `int`. Has topology caching via `getTopology()`.

### buildScopeQueries (only in metricstoreclient):
- Clean standalone function handling all scope transformations
- Returns empty slices for expected exceptions, nil for unhandled cases
- Includes **MemoryDomain → Socket** transformation (MISSING from pkg/metricstore)

### buildNodeQueries differences:
- **pkg/metricstore**: Gets topology inline per node. Uses `map[schema.MetricScope]bool` dedup. Has inline scope transformation.
- **metricstoreclient**: Uses `getTopology()`/`getTopologyByNode()` with caching. Uses labeled loop dedup. Delegates to `buildScopeQueries()`.

### APIQuery type differences:
- **pkg/metricstore**: `Resolution int64`, has `ScaleFactor schema.Float`
- **metricstoreclient**: `Resolution int`, no ScaleFactor

### sanitizeStats differences:
- **pkg/metricstore**: `sanitizeStats(data *APIMetricData)` — zeros each independently
- **metricstoreclient**: `sanitizeStats(avg, min, max *schema.Float)` — zeros all if any is NaN. Also has `hasNaNStats()` helper.

### LoadData/LoadStats/LoadScopedStats/LoadNodeData/LoadNodeListData:
- Nearly identical result-processing logic
- Different data fetching: `FetchData(req)` vs `ccms.doRequest(ctx, &req)`
- Internal store has `TestLoadDataCallback` test hook
- External client has topology caching

### Missing from pkg/metricstore:
- **MemoryDomain → Socket** scope transformation (present in metricstoreclient)

### Missing from metricstoreclient:
- Nothing significant missing from metricstoreclient

## Consolidation Approach

The user said metricstoreclient is the cleaner implementation. I agree — `buildScopeQueries()` as a standalone function is much cleaner than the inline if-chain.

Design a plan to:
1. Extract `buildScopeQueries()` as a shared helper (possibly in a shared package or as a package-level function in metricstore)
2. Synchronize the scope transformations (add MemoryDomain → Socket to internal)
3. Evaluate if result-processing logic can be shared
4. Consider type differences (int vs int64 Resolution)

Key constraints:
- The two packages have separate APIQuery types (different Resolution types, internal has ScaleFactor)
- The two packages use different data fetching mechanisms
- Must not break the MetricDataRepository interface contract
- Keep changes minimal and focused

Provide a detailed implementation plan.


Now let me write the final plan.


Write

# Consolidate buildQueries / Scope Transformation Logic

## Context

`pkg/metricstore/query.go` (InternalMetricStore) and `internal/metricstoreclient/cc-metric-store-queries.go` (CCMetricStore) both implement the `MetricDataRepository` interface. They contain nearly identical scope-transformation logic for building metric queries, but the code has diverged:

- **metricstoreclient** has the cleaner design: a standalone `buildScopeQueries()` function handling all transformations
- **metricstore** has the same logic inlined as a long if-chain inside `buildQueries()` and duplicated in `buildNodeQueries()`
- **metricstore is missing** the `MemoryDomain → Socket` transformation that metricstoreclient has
- Both packages duplicate scope string constants, `intToStringSlice`, and `sanitizeStats`

Goal: Extract shared scope-transformation logic, synchronize behavior, and eliminate duplication.

## Approach

Extract `BuildScopeQueries` as a shared exported function in `pkg/metricstore/` (since metricstoreclient already imports it). Return a package-independent intermediate type (`ScopeQueryResult`) that each package converts to its own `APIQuery` — this avoids coupling the two different `APIQuery` types (they differ in `Resolution` type: `int64` vs `int`, and internal has `ScaleFactor`).

## Steps

### 1. Create `pkg/metricstore/scopequery.go`

New file containing:

- **`ScopeQueryResult`** struct — intermediate type with fields: `Metric`, `Hostname`, `Aggregate`, `Type *string`, `TypeIds []string`, `Scope schema.MetricScope` (no Resolution — callers set it)
- **`BuildScopeQueries()`** — exported function ported from `metricstoreclient/buildScopeQueries()` (lines 257-517), returning `([]ScopeQueryResult, error)`. Returns empty slice for expected exceptions, error for unhandled cases
- **Scope string constants** — `HWThreadString`, `CoreString`, `MemoryDomainString`, `SocketString`, `AcceleratorString` (exported, move from both packages)
- **`IntToStringSlice()`** — exported, using the optimized buffer-reuse version from metricstore
- **`SanitizeStats(avg, min, max *schema.Float)`** — exported, using the metricstoreclient behavior (zero ALL if ANY is NaN)

### 2. Update `pkg/metricstore/query.go`

- Replace inline if-chain in `buildQueries()` (lines 317-534) with call to `BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Replace inline if-chain in `buildNodeQueries()` (lines 1099-1316) with same pattern
- Delete local scope string constants, `intToStringSlice`, local `sanitizeStats`
- Update `sanitizeStats` calls in `LoadData`/`LoadScopedStats`/`LoadNodeData`/`LoadNodeListData` to use `SanitizeStats(&res.Avg, &res.Min, &res.Max)`
- Switch scope dedup from `map[schema.MetricScope]bool` to labeled-loop + slice (matches metricstoreclient, avoids allocation for typical 1-3 scopes)
- This **adds the missing MemoryDomain → Socket transformation** to the internal store

### 3. Update `internal/metricstoreclient/cc-metric-store-queries.go`

- Replace call to local `buildScopeQueries()` with `metricstore.BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Delete local `buildScopeQueries()` function
- Delete local scope string constants and `intToStringSlice`
- Update `sanitizeStats` in `cc-metric-store.go` to call `metricstore.SanitizeStats()`

### 4. Add tests: `pkg/metricstore/scopequery_test.go`

Table-driven tests for `BuildScopeQueries` covering all transformation cases:
- Same-scope (HWThread→HWThread, Core→Core, Socket→Socket, Node→Node, etc.)
- Aggregation (HWThread→Core, HWThread→Socket, HWThread→Node, Core→Socket, Core→Node, etc.)
- Accelerator cases (Accelerator→Accelerator, Accelerator→Node)
- MemoryDomain cases including MemoryDomain→Socket
- Unhandled case returns error

## Files Modified

| File | Action |
|------|--------|
| `pkg/metricstore/scopequery.go` | **New** — shared types and functions |
| `pkg/metricstore/scopequery_test.go` | **New** — tests for BuildScopeQueries |
| `pkg/metricstore/query.go` | Refactor buildQueries/buildNodeQueries to use BuildScopeQueries; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store-queries.go` | Replace local buildScopeQueries with shared; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store.go` | Update sanitizeStats to use shared SanitizeStats |

## What is NOT shared (intentionally)

- **`APIQuery`/`APIMetricData` types** — different Resolution types, Data types, ScaleFactor field
- **`buildQueries`/`buildNodeQueries` themselves** — different topology access (caching vs inline), different receiver types
- **Load* result-processing functions** — depend on package-specific types and fetch mechanisms

## Verification

1. `go build ./...` — compile check
2. `go test ./pkg/metricstore/...` — existing + new scopequery tests
3. `go test ./internal/metricstoreclient/...` — compile check
4. `make test` — full test suite


File created successfully at: /Users/jan/.claude/plans/curious-dancing-nebula.md

# Consolidate buildQueries / Scope Transformation Logic

## Context

`pkg/metricstore/query.go` (InternalMetricStore) and `internal/metricstoreclient/cc-metric-store-queries.go` (CCMetricStore) both implement the `MetricDataRepository` interface. They contain nearly identical scope-transformation logic for building metric queries, but the code has diverged:

- **metricstoreclient** has the cleaner design: a standalone `buildScopeQueries()` function handling all transformations
- **metricstore** has the same logic inlined as a long if-chain inside `buildQueries()` and duplicated in `buildNodeQueries()`
- **metricstore is missing** the `MemoryDomain → Socket` transformation that metricstoreclient has
- Both packages duplicate scope string constants, `intToStringSlice`, and `sanitizeStats`

Goal: Extract shared scope-transformation logic, synchronize behavior, and eliminate duplication.

## Approach

Extract `BuildScopeQueries` as a shared exported function in `pkg/metricstore/` (since metricstoreclient already imports it). Return a package-independent intermediate type (`ScopeQueryResult`) that each package converts to its own `APIQuery` — this avoids coupling the two different `APIQuery` types (they differ in `Resolution` type: `int64` vs `int`, and internal has `ScaleFactor`).

## Steps

### 1. Create `pkg/metricstore/scopequery.go`

New file containing:

- **`ScopeQueryResult`** struct — intermediate type with fields: `Metric`, `Hostname`, `Aggregate`, `Type *string`, `TypeIds []string`, `Scope schema.MetricScope` (no Resolution — callers set it)
- **`BuildScopeQueries()`** — exported function ported from `metricstoreclient/buildScopeQueries()` (lines 257-517), returning `([]ScopeQueryResult, error)`. Returns empty slice for expected exceptions, error for unhandled cases
- **Scope string constants** — `HWThreadString`, `CoreString`, `MemoryDomainString`, `SocketString`, `AcceleratorString` (exported, move from both packages)
- **`IntToStringSlice()`** — exported, using the optimized buffer-reuse version from metricstore
- **`SanitizeStats(avg, min, max *schema.Float)`** — exported, using the metricstoreclient behavior (zero ALL if ANY is NaN)

### 2. Update `pkg/metricstore/query.go`

- Replace inline if-chain in `buildQueries()` (lines 317-534) with call to `BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Replace inline if-chain in `buildNodeQueries()` (lines 1099-1316) with same pattern
- Delete local scope string constants, `intToStringSlice`, local `sanitizeStats`
- Update `sanitizeStats` calls in `LoadData`/`LoadScopedStats`/`LoadNodeData`/`LoadNodeListData` to use `SanitizeStats(&res.Avg, &res.Min, &res.Max)`
- Switch scope dedup from `map[schema.MetricScope]bool` to labeled-loop + slice (matches metricstoreclient, avoids allocation for typical 1-3 scopes)
- This **adds the missing MemoryDomain → Socket transformation** to the internal store

### 3. Update `internal/metricstoreclient/cc-metric-store-queries.go`

- Replace call to local `buildScopeQueries()` with `metricstore.BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Delete local `buildScopeQueries()` function
- Delete local scope string constants and `intToStringSlice`
- Update `sanitizeStats` in `cc-metric-store.go` to call `metricstore.SanitizeStats()`

### 4. Add tests: `pkg/metricstore/scopequery_test.go`

Table-driven tests for `BuildScopeQueries` covering all transformation cases:
- Same-scope (HWThread→HWThread, Core→Core, Socket→Socket, Node→Node, etc.)
- Aggregation (HWThread→Core, HWThread→Socket, HWThread→Node, Core→Socket, Core→Node, etc.)
- Accelerator cases (Accelerator→Accelerator, Accelerator→Node)
- MemoryDomain cases including MemoryDomain→Socket
- Unhandled case returns error

## Files Modified

| File | Action |
|------|--------|
| `pkg/metricstore/scopequery.go` | **New** — shared types and functions |
| `pkg/metricstore/scopequery_test.go` | **New** — tests for BuildScopeQueries |
| `pkg/metricstore/query.go` | Refactor buildQueries/buildNodeQueries to use BuildScopeQueries; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store-queries.go` | Replace local buildScopeQueries with shared; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store.go` | Update sanitizeStats to use shared SanitizeStats |

## What is NOT shared (intentionally)

- **`APIQuery`/`APIMetricData` types** — different Resolution types, Data types, ScaleFactor field
- **`buildQueries`/`buildNodeQueries` themselves** — different topology access (caching vs inline), different receiver types
- **Load* result-processing functions** — depend on package-specific types and fetch mechanisms

## Verification

1. `go build ./...` — compile check
2. `go test ./pkg/metricstore/...` — existing + new scopequery tests
3. `go test ./internal/metricstoreclient/...` — compile check
4. `make test` — full test suite


ExitPlanMode

# Consolidate buildQueries / Scope Transformation Logic

## Context

`pkg/metricstore/query.go` (InternalMetricStore) and `internal/metricstoreclient/cc-metric-store-queries.go` (CCMetricStore) both implement the `MetricDataRepository` interface. They contain nearly identical scope-transformation logic for building metric queries, but the code has diverged:

- **metricstoreclient** has the cleaner design: a standalone `buildScopeQueries()` function handling all transformations
- **metricstore** has the same logic inlined as a long if-chain inside `buildQueries()` and duplicated in `buildNodeQueries()`
- **metricstore is missing** the `MemoryDomain → Socket` transformation that metricstoreclient has
- Both packages duplicate scope string constants, `intToStringSlice`, and `sanitizeStats`

Goal: Extract shared scope-transformation logic, synchronize behavior, and eliminate duplication.

## Approach

Extract `BuildScopeQueries` as a shared exported function in `pkg/metricstore/` (since metricstoreclient already imports it). Return a package-independent intermediate type (`ScopeQueryResult`) that each package converts to its own `APIQuery` — this avoids coupling the two different `APIQuery` types (they differ in `Resolution` type: `int64` vs `int`, and internal has `ScaleFactor`).

## Steps

### 1. Create `pkg/metricstore/scopequery.go`

New file containing:

- **`ScopeQueryResult`** struct — intermediate type with fields: `Metric`, `Hostname`, `Aggregate`, `Type *string`, `TypeIds []string`, `Scope schema.MetricScope` (no Resolution — callers set it)
- **`BuildScopeQueries()`** — exported function ported from `metricstoreclient/buildScopeQueries()` (lines 257-517), returning `([]ScopeQueryResult, error)`. Returns empty slice for expected exceptions, error for unhandled cases
- **Scope string constants** — `HWThreadString`, `CoreString`, `MemoryDomainString`, `SocketString`, `AcceleratorString` (exported, move from both packages)
- **`IntToStringSlice()`** — exported, using the optimized buffer-reuse version from metricstore
- **`SanitizeStats(avg, min, max *schema.Float)`** — exported, using the metricstoreclient behavior (zero ALL if ANY is NaN)

### 2. Update `pkg/metricstore/query.go`

- Replace inline if-chain in `buildQueries()` (lines 317-534) with call to `BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Replace inline if-chain in `buildNodeQueries()` (lines 1099-1316) with same pattern
- Delete local scope string constants, `intToStringSlice`, local `sanitizeStats`
- Update `sanitizeStats` calls in `LoadData`/`LoadScopedStats`/`LoadNodeData`/`LoadNodeListData` to use `SanitizeStats(&res.Avg, &res.Min, &res.Max)`
- Switch scope dedup from `map[schema.MetricScope]bool` to labeled-loop + slice (matches metricstoreclient, avoids allocation for typical 1-3 scopes)
- This **adds the missing MemoryDomain → Socket transformation** to the internal store

### 3. Update `internal/metricstoreclient/cc-metric-store-queries.go`

- Replace call to local `buildScopeQueries()` with `metricstore.BuildScopeQueries()`, converting each `ScopeQueryResult` → local `APIQuery` (adding `Resolution`)
- Delete local `buildScopeQueries()` function
- Delete local scope string constants and `intToStringSlice`
- Update `sanitizeStats` in `cc-metric-store.go` to call `metricstore.SanitizeStats()`

### 4. Add tests: `pkg/metricstore/scopequery_test.go`

Table-driven tests for `BuildScopeQueries` covering all transformation cases:
- Same-scope (HWThread→HWThread, Core→Core, Socket→Socket, Node→Node, etc.)
- Aggregation (HWThread→Core, HWThread→Socket, HWThread→Node, Core→Socket, Core→Node, etc.)
- Accelerator cases (Accelerator→Accelerator, Accelerator→Node)
- MemoryDomain cases including MemoryDomain→Socket
- Unhandled case returns error

## Files Modified

| File | Action |
|------|--------|
| `pkg/metricstore/scopequery.go` | **New** — shared types and functions |
| `pkg/metricstore/scopequery_test.go` | **New** — tests for BuildScopeQueries |
| `pkg/metricstore/query.go` | Refactor buildQueries/buildNodeQueries to use BuildScopeQueries; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store-queries.go` | Replace local buildScopeQueries with shared; remove duplicated helpers |
| `internal/metricstoreclient/cc-metric-store.go` | Update sanitizeStats to use shared SanitizeStats |

## What is NOT shared (intentionally)

- **`APIQuery`/`APIMetricData` types** — different Resolution types, Data types, ScaleFactor field
- **`buildQueries`/`buildNodeQueries` themselves** — different topology access (caching vs inline), different receiver types
- **Load* result-processing functions** — depend on package-specific types and fetch mechanisms

## Verification

1. `go build ./...` — compile check
2. `go test ./pkg/metricstore/...` — existing + new scopequery tests
3. `go test ./internal/metricstoreclient/...` — compile check
4. `make test` — full test suite


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.


[Request interrupted by user for tool use]