---
phase: 67-expansion-sql-coverage-and-tech-debt-cleanup
plan: 04
type: execute
wave: 3
depends_on: ["67-03"]
files_modified:
  - test/integration/test_adbc_queries.py
autonomous: true
requirements: []
must_haves:
  truths:
    - "The `latest_qty` / `MIN_BY` semantic mismatch in `test_adbc_queries.py` scenario 4 (`test_semi_additive_non_default_schema`, lines ~238-289) is resolved per CONTEXT.md D-15."
    - "Resolution path attempted FIRST is the test-rename: if standard-SQL `MIN_BY(qty, snapshot_date)` returns the qty value at the EARLIEST `snapshot_date` (the row where `snapshot_date` is minimum), then the metric name `latest_qty` is misleading and the cheapest fix is renaming to `earliest_qty` in the scenario DDL + assertion args."
    - "Investigation step BEFORE making any rename: read scenario 4 of `test/integration/test_adbc_queries.py` in full (lines 238-289), then confirm the project's `MIN_BY` semantics align with standard SQL by inspecting (a) any RESEARCH.md note from Phase 47 if accessible, OR (b) the emitted SQL from `explain_semantic_view('inv_view', dimensions := ['warehouse'], metrics := ['latest_qty'])` to see what `MIN_BY` resolves to. Default per D-15: standard SQL semantics, rename is correct."
    - "Escalation gate per D-15: if the rename is contradicted by Phase 47 RESEARCH.md or by the emitted SQL (e.g. the project's `MIN_BY` is implemented to return the value at MAX `snapshot_date` despite the name), STOP and re-scope — escalation requires explicit reframing and would NOT land in this plan."
    - "`just test-all` exits 0 after the rename — scenario 4 passes with the renamed metric (or with an alternative resolution if rename is contradicted, surfacing the re-scoping requirement first)."
  artifacts:
    - path: "test/integration/test_adbc_queries.py"
      provides: "Scenario 4 metric name aligned with semantics (rename `latest_qty` -> `earliest_qty` if standard MIN_BY semantics confirmed)"
      contains: "earliest_qty"
  key_links:
    - from: "test_adbc_queries.py scenario 4 metric DDL"
      to: "test_adbc_queries.py scenario 4 assertion args"
      via: "metric name appears in both `METRICS (i.latest_qty AS MIN_BY(i.qty, i.snapshot_date))` and `metrics := ['latest_qty']` — rename must update both consistently"
      pattern: "earliest_qty|latest_qty"
---

<objective>
Resolve the `latest_qty` / `MIN_BY` semantic mismatch in `test/integration/test_adbc_queries.py` scenario 4. Per CONTEXT.md D-15: `MIN_BY(qty, snapshot_date)` returns the value of `qty` at the row where `snapshot_date` is MINIMUM (i.e. earliest) — but the metric is named `latest_qty`, which is misleading. The cheapest fix: rename the metric to `earliest_qty` to match standard SQL semantics.

Purpose per CONTEXT.md `<scope_fence>` and `<risks_to_propagate>`: bounded scope. Try the test rename first. Only audit project-wide `MIN_BY` semantics if the rename is contradicted by Phase 47 RESEARCH.md or by the emitted SQL. Escalation requires re-scoping — NOT a continuation of this plan.

Output:
- Read scenario 4 in full to confirm exact metric definition + observed behavior.
- Inspect emitted SQL via `explain_semantic_view('inv_view', ...)` to confirm `MIN_BY` resolution semantics.
- If standard semantics confirmed: rename `latest_qty` → `earliest_qty` across the scenario DDL and assertion args.
- If contradicted (Case B): STOP, document finding in SUMMARY, do NOT land any code change in this plan; escalate to the user with explicit re-scoping per D-15.
- `just test-all` exits 0 either way (Case A: rename landed and passes; Case B: no code change, existing 7/7 ADBC PASS preserved while awaiting re-scoping).
</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/67-expansion-sql-coverage-and-tech-debt-cleanup/67-CONTEXT.md
@.planning/phases/66-expansion-qualification-adbc-tests/66-REVIEW.md
@.planning/phases/66-expansion-qualification-adbc-tests/66-REVIEW-FIX.md

# Investigation target
@test/integration/test_adbc_queries.py

# Project rules
@CLAUDE.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Investigate scenario 4 — confirm MIN_BY semantics before renaming</name>
  <files>(no files modified — investigation step)</files>
  <read_first>
    - test/integration/test_adbc_queries.py lines 238-289 (the full `test_semi_additive_non_default_schema` scenario): note the metric DDL `i.latest_qty AS MIN_BY(i.qty, i.snapshot_date)`, the test data (`(1, 'WH1', DATE '2026-01-01', 10), (2, 'WH1', DATE '2026-01-02', 15), (3, 'WH2', DATE '2026-01-01', 20), (4, 'WH2', DATE '2026-01-02', 25)`), and the assertion (`rows == 2`).
    - `.planning/phases/66-expansion-qualification-adbc-tests/66-REVIEW-FIX.md` WR-02 entry (around line 130-135): "For scenario 4 `MIN_BY(qty, snapshot_date)` would yield `latest_qty = 10` for `WH1` (earliest snapshot 2026-01-01 has qty=10) — but the metric is named `latest_qty` and is computed at the *minimum* snapshot_date, suggesting either the metric name is misleading or the reviewer's example value is mislabelled."
    - DuckDB documentation for `MIN_BY` (via Context7 MCP if accessible; standard SQL semantics: `MIN_BY(x, y)` returns the value of `x` corresponding to the MINIMUM value of `y`)
    - .planning/phases/47*/ if any RESEARCH.md from the original semi-additive metric implementation exists (it likely does not — Phase 47 was v0.6.0 and may not have a per-phase RESEARCH.md from the v0.5.x era)
    - If accessible: `src/expand/semi_additive.rs` to see if `MIN_BY` gets any special handling beyond the standard SQL aggregation
  </read_first>
  <action>
1. Read scenario 4 in full (lines 238-289 of `test/integration/test_adbc_queries.py`). Confirm:
   - Metric DDL: `METRICS (i.latest_qty AS MIN_BY(i.qty, i.snapshot_date))`
   - Test data rows: WH1 has snapshots 2026-01-01 (qty=10) and 2026-01-02 (qty=15); WH2 has snapshots 2026-01-01 (qty=20) and 2026-01-02 (qty=25).
   - Assertion: `rows == 2` (just a row-count check, no value check).

2. Reason about what `MIN_BY(qty, snapshot_date)` returns under standard SQL semantics:
   - `MIN_BY(x, y)` returns `x` from the row where `y` is minimum (across the group).
   - Per-group (`warehouse`): WH1 min snapshot_date is 2026-01-01, so `MIN_BY(qty, snapshot_date)` for WH1 returns `qty = 10`. WH2 min snapshot_date is 2026-01-01, returns `qty = 20`.
   - Result: `MIN_BY` returns the qty at the EARLIEST snapshot_date — so the value labelled `latest_qty` is actually the EARLIEST qty.

3. Empirically verify by inspecting the emitted SQL. If a quick DuckDB session is available (e.g. via `cargo run --bin duckdb-cli` or the `examples/` Python shell), run:
   - `LOAD semantic_views;`
   - Recreate the scenario 4 view in-memory.
   - `SELECT * FROM explain_semantic_view('inv_view', dimensions := ['warehouse'], metrics := ['latest_qty']);`
   - Look for `MIN_BY` in the emitted SQL OR the equivalent CASE-WHEN-with-ROW_NUMBER pattern (semi-additive metrics get rewritten to ROW_NUMBER snapshot-CTE per `src/expand/semi_additive.rs`). The expansion may resolve `MIN_BY` to something like `MIN(qty)` over a snapshot-row-1 filter — confirm.

4. If feasible, also empirically check the VALUE: query the scenario 4 view and verify which qty is returned per warehouse:
   - Expected if standard semantics: `(WH1, 10), (WH2, 20)` — the earliest-snapshot qty.
   - If actually `(WH1, 15), (WH2, 25)`: the project's `MIN_BY` is implemented backwards (would be a real bug, escalation territory).

5. Document the finding. SCOPE.md C4 establishes the ambiguity is real; D-15 contemplates only two terminal outcomes. The investigation MUST conclude in one of these — pure investigation without a terminal outcome is NOT acceptable (D-15 / Issue 3):
   - **Case A (standard semantics confirmed):** Standard `MIN_BY(qty, snapshot_date)` returns earliest-snapshot qty. The metric name `latest_qty` is misleading. Proceed to Task 2 (rename).
   - **Case B (semantics contradicted or rename otherwise blocked):** The project's `MIN_BY` returns latest-snapshot qty (which would make `latest_qty` correct but `MIN_BY` semantically inverted vs. standard SQL) — OR any other concrete finding contradicting the rename. STOP. Do NOT land Task 2's rename. Surface this as an explicit re-scoping requirement in SUMMARY with the evidence — this triggers user re-scoping per D-15, NOT a quiet pass-through.

6. Record the investigation outcome in `/tmp/claude/c4_investigation_67_04.log` so Task 2 (or its skip) can reference the evidence. The log MUST classify as Case A or Case B — no third "no-change-required" terminal state.
  </action>
  <verify>
    <automated>cat test/integration/test_adbc_queries.py | sed -n '238,290p' > /tmp/claude/c4_scenario_67_04.txt; wc -l /tmp/claude/c4_scenario_67_04.txt; echo "Investigation manual; see /tmp/claude/c4_investigation_67_04.log for the conclusion (Case A / B / C)"</automated>
  </verify>
  <acceptance_criteria>
    - Scenario 4 source (lines 238-289) read and the metric DDL confirmed as `MIN_BY(qty, snapshot_date)`.
    - Case classification (A or B — terminal outcomes only per D-15 / Issue 3) recorded in `/tmp/claude/c4_investigation_67_04.log` with the reasoning. Investigation that does not terminate in A or B is unacceptable — re-investigate until one of the two outcomes holds.
    - If Case A: proceed to Task 2.
    - If Case B: STOP. Plan output is the SUMMARY documenting the re-scoping need with evidence; Task 2 is skipped; user is explicitly notified that re-scoping is required.
  </acceptance_criteria>
  <done>Investigation complete; case classified as A or B (no other terminal state); finding logged. Task 2 either proceeds (Case A) or is skipped pending user re-scoping (Case B).</done>
</task>

<task type="auto">
  <name>Task 2 (Case A only): Rename latest_qty → earliest_qty in scenario 4 + run full quality gate</name>
  <files>test/integration/test_adbc_queries.py</files>
  <read_first>
    - test/integration/test_adbc_queries.py lines 238-289 (the same scenario; identify every occurrence of `latest_qty` — likely two: the metric DDL at the `METRICS (...)` clause, and the assertion arg `metrics := ['latest_qty']`)
    - The investigation log from Task 1 (`/tmp/claude/c4_investigation_67_04.log`) confirming Case A
  </read_first>
  <action>
**Skip this task if Task 1 classified as Case B (D-15 escalation).** Document the skip in the SUMMARY with the re-scoping requirement.

If Case A:

1. In `test/integration/test_adbc_queries.py` scenario 4 (`test_semi_additive_non_default_schema`), rename every occurrence of `latest_qty` to `earliest_qty`:
   - The metric DDL: `METRICS (i.latest_qty AS MIN_BY(i.qty, i.snapshot_date))` → `METRICS (i.earliest_qty AS MIN_BY(i.qty, i.snapshot_date))`
   - The assertion arg: `metrics := ['latest_qty']` → `metrics := ['earliest_qty']`
   - The docstring opening (line 239): `"""Scenario 4 — semi-additive metric, non-default schema base table. ACTIVE.` — leave structural framing alone, but if the docstring narrative mentions `latest_qty` by name, update for consistency.

2. Run `grep -n 'latest_qty' test/integration/test_adbc_queries.py` after the rename to confirm zero remaining hits (the metric is scoped to scenario 4 only; no other scenario references it). If any remain in other scenarios, surface — they shouldn't exist, but defense-in-depth.

3. Run the test:
   - `just test-adbc-queries > /tmp/claude/test_adbc_67_04_task2.log 2>&1; RC=$?; tail -80 /tmp/claude/test_adbc_67_04_task2.log; test "$RC" -eq 0`
   - Expected: 7/7 PASS, 0 FAIL. Scenario 4 still passes because:
     - The metric DDL parses (any valid identifier works for the metric name)
     - The assertion still returns `rows == 2` (row count is unchanged by metric rename)

4. Run the full quality gate:
   - `just test-all > /tmp/claude/test_all_67_04_task2.log 2>&1; RC=$?; tail -120 /tmp/claude/test_all_67_04_task2.log; test "$RC" -eq 0`
   - Expected: 0.

5. Commit message: `fix(67): rename latest_qty -> earliest_qty per MIN_BY semantics (C4 / D-15)`.

6. If `mktemp` is blocked, retry with `dangerouslyDisableSandbox: true` per CLAUDE.md Rule 2.
  </action>
  <verify>
    <automated>grep -cn "latest_qty" test/integration/test_adbc_queries.py > /tmp/claude/grep_latest_qty_67_04.log; cat /tmp/claude/grep_latest_qty_67_04.log; grep -cn "earliest_qty" test/integration/test_adbc_queries.py > /tmp/claude/grep_earliest_qty_67_04.log; cat /tmp/claude/grep_earliest_qty_67_04.log; just test-adbc-queries > /tmp/claude/test_adbc_67_04_task2.log 2>&1; RC=$?; tail -80 /tmp/claude/test_adbc_67_04_task2.log; test "$RC" -eq 0 && just test-all > /tmp/claude/test_all_67_04_task2.log 2>&1; RC=$?; tail -120 /tmp/claude/test_all_67_04_task2.log; test "$RC" -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - (Case A only) `grep -c "latest_qty" test/integration/test_adbc_queries.py` returns 0.
    - (Case A only) `grep -c "earliest_qty" test/integration/test_adbc_queries.py` returns >= 2 (metric DDL + assertion arg, minimum).
    - (Case A only) `just test-adbc-queries` exits 0 with 7/7 PASS.
    - (Case A only) `just test-all` exits 0.
    - (Case A only) Atomic commit `fix(67): rename latest_qty -> earliest_qty per MIN_BY semantics (C4 / D-15)`.
    - (Case B) Task skipped; SUMMARY documents the case classification, the evidence, and the explicit re-scoping requirement for user attention.
  </acceptance_criteria>
  <done>Case A: rename landed, 7/7 ADBC green, full test-all green, atomic commit. Case B: re-scoping documented in SUMMARY with evidence, no code change, user explicitly notified.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none new) | This plan modifies (or does not modify) a single test file; no new untrusted input, no new FFI, no new SQL emission. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-67-04-01 | (none) | n/a | accept | No new threat surface; the rename is metric-name-only and changes no SQL behaviour, no data path, no FFI shape. The Case B / C outcomes also introduce no threat surface (no code change in those branches). |

No new threats. C4 is a pure correctness-of-naming fix (or, per investigation, a no-op).
</threat_model>

<verification>
- Task 1 investigation logs the case classification in `/tmp/claude/c4_investigation_67_04.log`.
- If Case A: scenario 4 metric renamed; `just test-adbc-queries` exits 0 with 7/7 PASS; `just test-all` exits 0.
- If Case B: re-scoping requirement documented in SUMMARY with evidence; no code change; existing 7/7 ADBC PASS preserved; user notified for re-scoping per D-15.
- Tail-buffer rule honored: all build/test commands redirect to `/tmp/claude/*.log` first.
- Sandbox bypass used only when `mktemp` blocks build/test, per CLAUDE.md Rule 2.
</verification>

<success_criteria>
- Investigation completed and case classified (A or B — terminal outcomes only per D-15) with reasoning.
- Case A: metric rename landed; tests green; atomic commit.
- Case B: plan output is the re-scoping document with evidence; no code change; user explicitly notified.
- No release-prep tasks included.
- No scope creep into project-wide `MIN_BY` audit unless Case B explicitly triggers re-scoping.
</success_criteria>

<output>
Create `.planning/phases/67-expansion-sql-coverage-and-tech-debt-cleanup/67-04-SUMMARY.md` when done. Include:
- The case classification (A or B — terminal outcomes only per D-15) with the investigation evidence
- (Case A) The commit SHA + message + the diff snippet for the rename
- (Case B) The re-scoping document: what was contradicted, what evidence supports the contradiction, what scope a re-plan would need to cover, and an explicit "user re-scoping required" callout
- Confirmation that `just test-adbc-queries` and (Case A only) `just test-all` exited 0
- If Case A: confirmation that NO scenario other than scenario 4 referenced `latest_qty` by name (defense-in-depth grep)
</output>
