---
phase: 68
plan: 03
type: execute
wave: 2
depends_on: [68-01]
files_modified:
  - src/body_parser.rs
  - test/sql/phase68_quoted_idents_non_additive.test
  - test/sql/phase68_quoted_idents_window.test
  - test/sql/TEST_LIST
autonomous: true
scope_items: [B1, B2]
threat_refs: [T-68-07]
must_haves:
  truths:
    - "`parse_non_additive_dims` accepts a quoted identifier with internal whitespace as a dimension name (e.g., `NON ADDITIVE BY (\"my dim\" DESC)`) — the quoted segment is captured intact, no `split_whitespace` shredding (B1)."
    - "`parse_non_additive_dims` accepts a dotted path as a dimension name (e.g., `NON ADDITIVE BY (orders.\"my dim\" DESC)`) per D-08 contract extension (B1)."
    - "`parse_window_spec` OVER ORDER BY accepts a quoted identifier with internal whitespace (e.g., `OVER (PARTITION BY EXCLUDING r ORDER BY \"order date\" ASC NULLS LAST)`) — quoted segment intact (B2)."
    - "`parse_window_spec` OVER ORDER BY accepts a dotted path per D-08 (B2)."
    - "Both new sqllogictest fixtures (`phase68_quoted_idents_non_additive.test` + `phase68_quoted_idents_window.test`) are registered in `test/sql/TEST_LIST` and pass under `just test-sql` (D-09)."
  artifacts:
    - path: "src/body_parser.rs"
      provides: "parse_non_additive_dims + parse_window_spec ORDER BY entry — identifier-aware tokenisation via find_identifier_end + balanced-quote check (mirroring Plan 68-01 Task 2's is_quoting_balanced helper)"
      contains: "find_identifier_end"
    - path: "test/sql/phase68_quoted_idents_non_additive.test"
      provides: "End-to-end coverage: CREATE TABLE with quoted-whitespace columns, CREATE SEMANTIC VIEW with NON ADDITIVE BY referencing them, semantic_view query returning expected rows. Includes at least one dotted-path test."
      contains: "NON ADDITIVE BY"
    - path: "test/sql/phase68_quoted_idents_window.test"
      provides: "End-to-end coverage: window-metric DDL with OVER ORDER BY referencing a quoted-whitespace column + a dotted path."
      contains: "OVER"
    - path: "test/sql/TEST_LIST"
      provides: "Two new entries registering the Phase 68 fixtures"
      contains: "phase68_quoted_idents_"
  key_links:
    - from: "src/body_parser.rs::parse_non_additive_dims"
      to: "src/ident.rs::find_identifier_end"
      via: "single call per dim entry capturing the identifier prefix"
      pattern: "find_identifier_end\\("
    - from: "src/body_parser.rs::parse_window_spec (ORDER BY arm at line 1725)"
      to: "src/ident.rs::find_identifier_end"
      via: "single call per ORDER BY entry capturing the column-reference prefix"
      pattern: "find_identifier_end\\("
    - from: "test/sql/TEST_LIST"
      to: "test/sql/phase68_quoted_idents_*.test"
      via: "filename registration (CLAUDE.md hard rule — runner silently skips unlisted files)"
      pattern: "phase68_quoted_idents"
---

<objective>
Land TECH-DEBT #25 — the two sibling `split_whitespace` sites in `src/body_parser.rs` that Phase 67 Plan 02's audit-grep flagged as identifier-tokenisation bugs. Both sites are in the same shape class as Phase 67's `parse_single_table_entry` fix (TECH-DEBT #24), and per RESEARCH §B1/§B2 the (c)-class structural-rewrite classification is pessimistic — both call sites receive raw `&str` content (post-paren-extraction, pre-tokenisation), so porting `find_identifier_end` is a (b)-class loop refactor (~15-25 LOC per site, well within budget).

Per CONTEXT.md D-08: this plan also extends both contracts to accept dotted paths (`table.col`). `find_identifier_end` natively walks across dots outside quoted regions, so the contract extension is free with the port. No existing fixture exercises the dotted-path case, so the extension is purely additive — verified by RESEARCH §B1 and §B2.

Per D-09: each site requires its own dedicated sqllogictest fixture with at least one quoted-whitespace test row + at least one dotted-path test row. Both fixtures MUST be registered in `test/sql/TEST_LIST` (CLAUDE.md hard rule).

This plan depends on 68-01 (Wave 1) so the A1 reserved-keyword guard pattern from 68-01 Task 1 is the established parser-consistency reference when B1/B2 land — the dependency is soft (per RESEARCH Landmine #8) but per CONTEXT.md sequencing we keep it.

Purpose: Close out TECH-DEBT #25 before v0.10.0 milestone tag. Restore identifier-aware tokenisation parity across the three sibling parser sites (`parse_single_table_entry`, `parse_non_additive_dims`, `parse_window_spec` ORDER BY).

Output: One commit covering B1 (parser + Rust unit tests + sqllogictest fixture + TEST_LIST entry) and one commit covering B2 (same shape). 4 files modified, 2 new fixtures.
</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/STATE.md
@.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-CONTEXT.md
@.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/SCOPE.md
@.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-RESEARCH.md
@.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-VALIDATION.md
@.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-01-PLAN.md
@.planning/phases/67-expansion-sql-coverage-and-tech-debt-cleanup/67-02-PLAN.md
@TECH-DEBT.md
@CLAUDE.md

<interfaces>
<!-- Current shape of the two sibling sites in src/body_parser.rs. Extracted verbatim. -->
<!-- Both receive raw &str content. The port pattern is identical to parse_single_table_entry post-A3. -->

From `src/body_parser.rs:1404-1490` (B1 — parse_non_additive_dims):

```rust
fn parse_non_additive_dims(
    content: &str,
    base_offset: usize,
) -> Result<Vec<NonAdditiveDim>, ParseError> {
    let entries = split_at_depth0_commas(content);
    let mut result = Vec::new();
    for (start, entry_text) in entries {
        let entry_text = entry_text.trim();
        if entry_text.is_empty() {
            continue; // trailing comma
        }
        let parts: Vec<&str> = entry_text.split_whitespace().collect();  // ← B1 bug: shreds quoted identifiers
        if parts.is_empty() {
            return Err(ParseError { /* Empty dimension */ });
        }
        let dim_name = parts[0].to_string();
        let upper_parts: Vec<String> = parts.iter().map(|p| p.to_ascii_uppercase()).collect();
        // ... while i < upper_parts.len() { match upper_parts[i] {ASC | DESC | NULLS | other → err} ... }
    }
    Ok(result)
}
```

From `src/body_parser.rs:1725-1791` (B2 — parse_window_spec ORDER BY arm):

```rust
let entries = split_at_depth0_commas(order_text);
for (start, entry_text) in entries {
    let entry_text = entry_text.trim();
    if entry_text.is_empty() { continue; }
    let parts: Vec<&str> = entry_text.split_whitespace().collect();  // ← B2 bug: same shred
    if parts.is_empty() { continue; }
    let dim_name = parts[0].to_string();
    // ... while idx < parts.len() { match parts[idx].to_ascii_uppercase() {ASC | DESC | NULLS | _ → break} ... }
    order_by.push(WindowOrderBy { expr: dim_name, order: sort, nulls });
}
```

From `src/ident.rs:186-220` (the helper to port in):

```rust
pub fn find_identifier_end(input: &str, allow_paren: bool) -> usize
// Returns input.len() saturating on unterminated quote.
// Walks across dots outside quoted regions (so dotted FQN is a single call).
// Doubled `""` inside `"..."` is an escape.
```

From `src/body_parser.rs` (Plan 68-01 Task 2's helper, which 68-03 reuses):

```rust
fn is_quoting_balanced(s: &str) -> bool
// Returns true iff `s` has matched `"` quotes accounting for `""` escape.
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: B1 — port parse_non_additive_dims to identifier-aware tokenisation + sqllogictest fixture + TEST_LIST entry</name>
  <files>src/body_parser.rs, test/sql/phase68_quoted_idents_non_additive.test, test/sql/TEST_LIST</files>
  <read_first>
    - src/body_parser.rs (lines 1404-1490 — current parse_non_additive_dims; lines 692-720 post-Plan-68-01 parse_single_table_entry — the analog to port from; the new is_quoting_balanced helper from 68-01 Task 2)
    - src/ident.rs (lines 186-220 — find_identifier_end semantics)
    - test/sql/phase67_quoted_source_tables.test (shape reference for the new fixture)
    - test/sql/TEST_LIST (registration site — append two new entries)
    - .planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-CONTEXT.md (D-07, D-08, D-09, D-10)
    - .planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-RESEARCH.md (Item-by-Item §B1 + §B1/B2 Deep-Dive — Landmine #3 (TEST_LIST registration))
    - .planning/phases/67-expansion-sql-coverage-and-tech-debt-cleanup/67-02-PLAN.md (Phase 67 Plan 02 — the precedent for this shape of port)
  </read_first>
  <behavior>
    - Test 1 (B1 quoted whitespace, unit): `parse_non_additive_dims("\"my dim\" DESC", 0)` returns `Ok(vec![NonAdditiveDim { dimension: "\"my dim\"".into(), order: Desc, nulls: First }])` — the quoted segment is captured intact, DESC sets NULLS FIRST default.
    - Test 2 (B1 dotted path, unit): `parse_non_additive_dims("orders.\"my dim\" ASC NULLS LAST, c2 DESC", 0)` returns 2 entries, the first with `dimension = "orders.\"my dim\""`.
    - Test 3 (B1 unterminated quote, unit): `parse_non_additive_dims("\"unclosed DESC", 0)` returns `Err(ParseError)` with a message containing `"Unterminated quoted identifier"` (mirroring 68-01 Task 2's contract).
    - Test 4 (B1 sqllogictest, e2e): a fresh fixture exercises the full path — CREATE TABLE with a `"my dim"` column, CREATE SEMANTIC VIEW with `NON ADDITIVE BY ("my dim" DESC)`, query returns expected rows.
    - Test 5 (B1 sqllogictest, dotted-path e2e): same fixture or a sibling block uses a dotted path.
  </behavior>
  <action>
    Step A — Port `parse_non_additive_dims` (src/body_parser.rs:1404-1490). Replace the `let parts: Vec<&str> = entry_text.split_whitespace().collect();` shred with identifier-aware tokenisation. Pattern (mirrors post-68-01 `parse_single_table_entry`):

    1. Call `let name_end = find_identifier_end(entry_text, /* allow_paren = */ false);` — `allow_paren=false` because NAB entries have no parens inside identifiers (per RESEARCH §B1/B2 Deep-Dive).
    2. If `name_end == 0`, return `Err(ParseError { message: "Empty dimension in NON ADDITIVE BY clause".to_string(), position: Some(base_offset + start) })` — preserve existing error.
    3. Run `is_quoting_balanced(&entry_text[..name_end])` (the helper landed in 68-01 Task 2). If `false`, return `Err(ParseError { message: format!("Unterminated quoted identifier in NON ADDITIVE BY dimension entry '{entry_text}'."), position: Some(base_offset + start) })`.
    4. `let dim_name = entry_text[..name_end].trim().to_string();` — captured identifier (including quotes and dots intact).
    5. `let suffix = entry_text[name_end..].trim();` — the ASC/DESC/NULLS modifier portion.
    6. Tokenise the suffix via `let parts: Vec<&str> = suffix.split_whitespace().collect();` — `split_whitespace` is correct here because the suffix has no quoted identifiers, only keyword tokens.
    7. Feed `parts` into the existing `while i < upper_parts.len()` loop logic UNCHANGED — start at `i = 0` (not `i = 1`) since the dim_name was captured separately. The loop's matchers (`ASC | DESC | NULLS | other → err`) remain identical.

    Net change: ~15-25 LOC swap. If the diff balloons past ~50 LOC body delta + ~50 LOC tests for B1 alone, HALT and surface a SUMMARY finding per D-10.

    Step B — Add Rust unit tests in `src/body_parser.rs::tests`:
    - `test_parse_non_additive_dims_quoted_identifier_with_whitespace` — asserts Test 1 above.
    - `test_parse_non_additive_dims_dotted_path` — asserts Test 2 above.
    - `test_parse_non_additive_dims_unterminated_quote` — asserts Test 3 above.
    - `test_parse_non_additive_dims_regression_bare_no_whitespace` — pin no regression for the bare `report_date DESC` case the existing fixtures use.

    Step C — Create `test/sql/phase68_quoted_idents_non_additive.test`. Structure (sqllogictest block form, no inline regex per CLAUDE.md):

    ```
    # Phase 68 Plan 03 (B1): NON ADDITIVE BY accepts quoted identifiers with literal
    # whitespace AND dotted paths. Mirrors TECH-DEBT #24's fix to parse_single_table_entry.

    statement ok
    CREATE TABLE p68_b1_orders (id BIGINT, "order date" DATE, qty BIGINT);

    statement ok
    INSERT INTO p68_b1_orders VALUES (1, DATE '2026-01-01', 10), (2, DATE '2026-01-02', 20);

    # Scenario 1 — quoted identifier with internal whitespace as NAB dimension
    statement ok
    CREATE SEMANTIC VIEW p68_b1_v1 AS
      TABLES (o AS p68_b1_orders PRIMARY KEY (id))
      DIMENSIONS (o."order date" AS o."order date")
      METRICS (o.qty_sum AS sum(o.qty) NON ADDITIVE BY ("order date" DESC));

    query I
    SELECT count(*) > 0 FROM semantic_view('p68_b1_v1', dimensions := ['"order date"'], metrics := ['qty_sum']);
    ----
    true

    # Scenario 2 — dotted path (D-08 contract extension)
    statement ok
    CREATE SEMANTIC VIEW p68_b1_v2 AS
      TABLES (o AS p68_b1_orders PRIMARY KEY (id))
      DIMENSIONS (o."order date" AS o."order date")
      METRICS (o.qty_sum AS sum(o.qty) NON ADDITIVE BY (o."order date" DESC));

    query I
    SELECT count(*) > 0 FROM semantic_view('p68_b1_v2', dimensions := ['"order date"'], metrics := ['qty_sum']);
    ----
    true

    # Cleanup
    statement ok
    DROP SEMANTIC VIEW p68_b1_v1;

    statement ok
    DROP SEMANTIC VIEW p68_b1_v2;

    statement ok
    DROP TABLE p68_b1_orders;
    ```

    The exact NAB column-reference shape (`"order date"` vs `o."order date"` vs the qualified form) MUST match what the existing extension expects — verify against `test/sql/phase47_semi_additive.test` for the canonical NAB DDL shape. Adjust column names / DDL keywords if the extension requires e.g. `NON ADDITIVE BY` only inside a specific metric clause. If the existing extension currently rejects `o."order date"` as a NAB target (vs. the unqualified form), narrow the dotted-path scenario to use a SECOND base-table column reference (e.g., `orders."order date"`) so the dotted-path port is exercised.

    Step D — Register the new fixture in `test/sql/TEST_LIST`. Append a line `test/sql/phase68_quoted_idents_non_additive.test` to the file. Per CLAUDE.md hard rule: runner silently skips files not in TEST_LIST. Verify the file is in `TEST_LIST` BEFORE running `just test-sql` (`grep -c phase68_quoted_idents_non_additive test/sql/TEST_LIST` MUST return `1`).

    Step E — Build and verify:

    ```
    just build > /tmp/claude/b1_build.log 2>&1
    RC=$?
    tail -30 /tmp/claude/b1_build.log
    [ $RC -eq 0 ] || exit 1

    cargo test --lib body_parser::tests::test_parse_non_additive_dims > /tmp/claude/b1_unit.log 2>&1
    RC=$?
    tail -30 /tmp/claude/b1_unit.log
    [ $RC -eq 0 ] || exit 1

    just test-sql phase68_quoted_idents_non_additive > /tmp/claude/b1_sql.log 2>&1
    RC=$?
    tail -40 /tmp/claude/b1_sql.log
    [ $RC -eq 0 ] || exit 1
    ```

    Step F — Stage and commit. Single commit message: `fix(68-03): B1 — identifier-aware tokenisation in parse_non_additive_dims; new sqllogictest fixture`.

    **Risk renegotiation gate (per D-10):** If during execution the port to `parse_non_additive_dims` exceeds ~50 LOC body delta OR requires lifting tokenisation out of the function (e.g., the suffix-parsing loop turns out to depend on raw-input character positions in an unexpected way), HALT and surface the finding in a SUMMARY note. Do NOT silently expand scope.
  </action>
  <verify>
    <automated>cargo test --lib body_parser::tests::test_parse_non_additive_dims &amp;&amp; just test-sql phase68_quoted_idents_non_additive</automated>
  </verify>
  <acceptance_criteria>
    - `src/body_parser.rs::parse_non_additive_dims` calls `find_identifier_end` — verifiable via `grep -A 30 'fn parse_non_additive_dims' src/body_parser.rs | grep -c 'find_identifier_end'` returning ≥ 1.
    - `src/body_parser.rs::parse_non_additive_dims` no longer relies on `entry_text.split_whitespace().collect()` to extract the dim name (the split MAY remain for the modifier suffix, but `parts[0]` must NOT be the dim-name source) — verifiable by code review of the diff.
    - 4 new unit tests in `body_parser::tests` pass: `test_parse_non_additive_dims_quoted_identifier_with_whitespace`, `test_parse_non_additive_dims_dotted_path`, `test_parse_non_additive_dims_unterminated_quote`, `test_parse_non_additive_dims_regression_bare_no_whitespace`.
    - `test/sql/phase68_quoted_idents_non_additive.test` exists and contains the substring `NON ADDITIVE BY` AND at least one quoted-whitespace identifier (e.g., `"order date"`) AND at least one dotted-path reference (e.g., `o."order date"`).
    - `test/sql/TEST_LIST` contains exactly one line `test/sql/phase68_quoted_idents_non_additive.test` — verifiable via `grep -c '^test/sql/phase68_quoted_idents_non_additive\.test$' test/sql/TEST_LIST` returning `1`.
    - `just test-sql phase68_quoted_idents_non_additive` exits 0.
    - Pre-existing fixtures still pass — `cargo test --lib body_parser` + `just test-sql phase47_semi_additive` + `just test-sql phase55_materialization_routing` all exit 0.
    - Pre-commit hook passes without `--no-verify`.
    - If body delta exceeds ~50 LOC, a SUMMARY finding documents the renegotiation per D-10.
  </acceptance_criteria>
  <done>B1 closed. NAB clause accepts quoted-whitespace + dotted-path. New fixture registered and green. Pre-existing NAB fixtures still pass.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: B2 — port parse_window_spec OVER ORDER BY to identifier-aware tokenisation + sqllogictest fixture + TEST_LIST entry</name>
  <files>src/body_parser.rs, test/sql/phase68_quoted_idents_window.test, test/sql/TEST_LIST</files>
  <read_first>
    - src/body_parser.rs (lines 1705-1799 — current parse_window_spec ORDER BY arm; the post-Task-1 parse_non_additive_dims for shape parity)
    - src/ident.rs (lines 186-220 — find_identifier_end semantics)
    - test/sql/phase48_window_metrics.test (existing window-metric fixture — shape reference)
    - test/sql/TEST_LIST
    - .planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-CONTEXT.md (D-07, D-08, D-09)
    - .planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-RESEARCH.md (Item-by-Item §B2 — including the open-question note about expression-shaped ORDER BY entries)
  </read_first>
  <behavior>
    - Test 1 (B2 quoted whitespace, unit): a `parse_window_spec` invocation with OVER ORDER BY entry `"order date" ASC NULLS LAST` produces a `WindowOrderBy { expr: "\"order date\"", order: Asc, nulls: Last }` — the quoted segment is captured intact.
    - Test 2 (B2 dotted path, unit): same with `orders."order date" ASC NULLS LAST` produces `WindowOrderBy { expr: "orders.\"order date\"", .. }`.
    - Test 3 (B2 unterminated quote, unit): an OVER ORDER BY entry with an unclosed `"` returns `Err(ParseError)` with a message containing `"Unterminated quoted identifier"`.
    - Test 4 (B2 sqllogictest, e2e): a fresh fixture exercises window-metric DDL with quoted-whitespace + dotted-path column references in OVER ORDER BY.
    - Test 5 (B2 regression, unit): pre-existing bare-identifier ORDER BY entries (`order_date ASC NULLS LAST`) still parse identically.
  </behavior>
  <action>
    Step A — Pre-check (RESEARCH Open Question #1): grep existing fixtures for expression-shaped OVER ORDER BY entries with function calls:

    ```
    grep -n "OVER (.*ORDER BY.*(" test/sql/*.test | head -20
    ```

    If any existing fixture passes an expression like `date_trunc('day', order_date) ASC` as an ORDER BY entry, HALT and surface a SUMMARY finding — the port narrows the contract from "expression" to "identifier", which is a behaviour change requiring renegotiation. If no expression-shaped entries exist (per RESEARCH §B2 expected outcome), proceed.

    Step B — Port `parse_window_spec` ORDER BY arm at `src/body_parser.rs:1725-1791`. Same pattern as Task 1's B1 port:

    1. Replace the `let parts: Vec<&str> = entry_text.split_whitespace().collect();` shred at line 1731 with:
       ```
       let name_end = find_identifier_end(entry_text, /* allow_paren = */ false);
       if name_end == 0 { continue; }  // preserve existing `parts.is_empty()` skip behaviour
       if !is_quoting_balanced(&entry_text[..name_end]) {
           return Err(ParseError {
               message: format!(
                   "Unterminated quoted identifier in OVER ORDER BY entry '{entry_text}'."
               ),
               position: Some(base_offset + start),
           });
       }
       let dim_name = entry_text[..name_end].trim().to_string();
       let suffix = entry_text[name_end..].trim();
       let parts: Vec<&str> = suffix.split_whitespace().collect();
       ```
    2. Adjust the existing `while idx < parts.len()` modifier loop to start at `idx = 0` (since dim_name is captured separately).
    3. The `WindowOrderBy { expr: dim_name, order: sort, nulls }` push remains unchanged at the bottom.

    Net change: ~20-30 LOC body delta. If significantly more, HALT per D-10.

    Step C — Add Rust unit tests in `src/body_parser.rs::tests`:
    - `test_parse_window_spec_quoted_order_by` — asserts Test 1 above (invokes `parse_window_spec` directly, or constructs the minimal containing DDL and routes through the public parser entry).
    - `test_parse_window_spec_dotted_order_by` — asserts Test 2 above.
    - `test_parse_window_spec_unterminated_quote_order_by` — asserts Test 3 above.
    - `test_parse_window_spec_regression_bare_order_by` — asserts Test 5 above.

    If `parse_window_spec` is not directly callable from tests (it may be a private helper), wrap the unit tests around the public DDL entry that flows through `parse_window_spec`, asserting the parsed `WindowOrderBy` via the assembled view structure.

    Step D — Create `test/sql/phase68_quoted_idents_window.test`. Structure (refer to `phase48_window_metrics.test` for the exact window-metric DDL grammar — common shape is `WINDOW (...) OVER (PARTITION BY EXCLUDING dim1, dim2 ORDER BY col ASC NULLS LAST)`):

    ```
    # Phase 68 Plan 03 (B2): OVER ORDER BY accepts quoted identifiers with literal whitespace
    # AND dotted paths. Sibling of B1's NON ADDITIVE BY fix.

    statement ok
    CREATE TABLE p68_b2_orders (id BIGINT, region VARCHAR, "order date" DATE, amount DOUBLE);

    statement ok
    INSERT INTO p68_b2_orders VALUES
      (1, 'US', DATE '2026-01-01', 100.0),
      (2, 'US', DATE '2026-01-02', 150.0),
      (3, 'EU', DATE '2026-01-01', 200.0);

    # Scenario 1 — quoted identifier with internal whitespace in OVER ORDER BY
    statement ok
    CREATE SEMANTIC VIEW p68_b2_v1 AS
      TABLES (o AS p68_b2_orders PRIMARY KEY (id))
      DIMENSIONS (o.region AS o.region, o."order date" AS o."order date")
      METRICS (o.running_total AS sum(o.amount) OVER (PARTITION BY EXCLUDING region ORDER BY "order date" ASC NULLS LAST));

    query I
    SELECT count(*) > 0 FROM semantic_view('p68_b2_v1', dimensions := ['region'], metrics := ['running_total']);
    ----
    true

    # Scenario 2 — dotted path in OVER ORDER BY (D-08 extension)
    statement ok
    CREATE SEMANTIC VIEW p68_b2_v2 AS
      TABLES (o AS p68_b2_orders PRIMARY KEY (id))
      DIMENSIONS (o.region AS o.region, o."order date" AS o."order date")
      METRICS (o.running_total AS sum(o.amount) OVER (PARTITION BY EXCLUDING region ORDER BY o."order date" ASC NULLS LAST));

    query I
    SELECT count(*) > 0 FROM semantic_view('p68_b2_v2', dimensions := ['region'], metrics := ['running_total']);
    ----
    true

    # Cleanup
    statement ok
    DROP SEMANTIC VIEW p68_b2_v1;

    statement ok
    DROP SEMANTIC VIEW p68_b2_v2;

    statement ok
    DROP TABLE p68_b2_orders;
    ```

    Adjust the exact DDL grammar to match what the extension parses today — refer to `test/sql/phase48_window_metrics.test` for the canonical OVER clause shape. If the metric expression with `WINDOW (...)` keyword wrapping is required, adopt the existing fixture's pattern verbatim.

    Step E — Append the new fixture to `test/sql/TEST_LIST`:

    ```
    test/sql/phase68_quoted_idents_window.test
    ```

    Verify: `grep -c '^test/sql/phase68_quoted_idents_window\.test$' test/sql/TEST_LIST` MUST return `1`.

    Step F — Build and verify:

    ```
    just build > /tmp/claude/b2_build.log 2>&1
    RC=$?
    tail -30 /tmp/claude/b2_build.log
    [ $RC -eq 0 ] || exit 1

    cargo test --lib body_parser::tests::test_parse_window_spec > /tmp/claude/b2_unit.log 2>&1
    RC=$?
    tail -30 /tmp/claude/b2_unit.log
    [ $RC -eq 0 ] || exit 1

    just test-sql phase68_quoted_idents_window > /tmp/claude/b2_sql.log 2>&1
    RC=$?
    tail -40 /tmp/claude/b2_sql.log
    [ $RC -eq 0 ] || exit 1

    just test-sql phase48_window_metrics > /tmp/claude/b2_regression.log 2>&1
    RC=$?
    tail -30 /tmp/claude/b2_regression.log
    [ $RC -eq 0 ] || exit 1
    ```

    Step G — Stage and commit. Single commit message: `fix(68-03): B2 — identifier-aware tokenisation in parse_window_spec OVER ORDER BY; new sqllogictest fixture`.

    **Risk renegotiation (per D-10):** If `parse_window_spec` is significantly more complex than the (b)-class port suggests (e.g., the ORDER BY entry slot turns out to consume expression-shaped input today), HALT and surface as a SUMMARY finding.
  </action>
  <verify>
    <automated>cargo test --lib body_parser::tests::test_parse_window_spec &amp;&amp; just test-sql phase68_quoted_idents_window &amp;&amp; just test-sql phase48_window_metrics</automated>
  </verify>
  <acceptance_criteria>
    - `src/body_parser.rs` `parse_window_spec` ORDER BY arm calls `find_identifier_end` — verifiable by grepping around line 1730 (post-edit) for `find_identifier_end`.
    - 4 new unit tests in `body_parser::tests` pass: `test_parse_window_spec_quoted_order_by`, `test_parse_window_spec_dotted_order_by`, `test_parse_window_spec_unterminated_quote_order_by`, `test_parse_window_spec_regression_bare_order_by`.
    - `test/sql/phase68_quoted_idents_window.test` exists and contains `OVER` + at least one quoted-whitespace identifier (`"order date"`) + at least one dotted-path reference (`o."order date"`).
    - `test/sql/TEST_LIST` contains exactly one line `test/sql/phase68_quoted_idents_window.test` — verifiable via `grep -c '^test/sql/phase68_quoted_idents_window\.test$' test/sql/TEST_LIST` returning `1`.
    - `just test-sql phase68_quoted_idents_window` exits 0.
    - Pre-existing window-metric fixture `just test-sql phase48_window_metrics` still exits 0 (no regression on bare identifier ORDER BY).
    - Pre-commit hook passes without `--no-verify`.
    - If body delta exceeds ~50 LOC, SUMMARY finding documents per D-10.
  </acceptance_criteria>
  <done>B2 closed. OVER ORDER BY accepts quoted-whitespace + dotted-path. Fixture registered + green. Pre-existing window-metric fixtures still pass.</done>
</task>

<task type="auto">
  <name>Task 3: Full-suite gate — just test-all + just ci</name>
  <files>(verification only — no source edits)</files>
  <read_first>
    - CLAUDE.md (Quality Gate section — `just test-all` is the verification command; `just ci` runs the CI mirror before push to main)
    - .planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-CONTEXT.md (Quality Gates)
  </read_first>
  <action>
    Step A — Run the full quality gate:

    ```
    just test-all > /tmp/claude/test_all.log 2>&1
    RC=$?
    tail -80 /tmp/claude/test_all.log
    [ $RC -eq 0 ] || exit 1
    ```

    Sandbox bypass acceptable per CLAUDE.md.

    Step B — Run the CI mirror (adds clippy pedantic + fmt + cargo-deny + fuzz target compile):

    ```
    just ci > /tmp/claude/ci.log 2>&1
    RC=$?
    tail -80 /tmp/claude/ci.log
    [ $RC -eq 0 ] || exit 1
    ```

    Step C — If either exits non-zero, do not commit the Task 3 verification artefact; instead diagnose, fix, and re-run. Note: this task does not produce a commit by itself — it gates the overall plan completion.
  </action>
  <verify>
    <automated>just test-all</automated>
  </verify>
  <acceptance_criteria>
    - `just test-all` exits 0 (Rust unit + proptest + sqllogictest + DuckLake CI + ADBC all green).
    - `just ci` exits 0 (test-all + clippy pedantic + fmt + cargo-deny + fuzz target compile all green).
    - Output logs at `/tmp/claude/test_all.log` and `/tmp/claude/ci.log` confirm both Phase 68 new fixtures (`phase68_quoted_idents_non_additive` and `phase68_quoted_idents_window`) appear in the sqllogictest output (i.e., they're not silently skipped — TEST_LIST registration is verified by their presence in the run output).
  </acceptance_criteria>
  <done>Full quality gate green. Both new Phase 68 sqllogictest fixtures are observed in the test-all output (TEST_LIST registration verified by behaviour, not just file content).</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Caller DDL → `parse_non_additive_dims` and `parse_window_spec` | Untrusted DDL text containing NON ADDITIVE BY or OVER ORDER BY clauses. Quoted identifiers with internal whitespace, dotted FQN paths, and unterminated quotes must all surface structured ParseError or correctly-captured identifiers, never silently-shredded malformed names that would corrupt downstream expansion. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-68-07 | Tampering | `parse_non_additive_dims` and `parse_window_spec` ORDER BY arm shredding quoted identifiers via `split_whitespace`, producing silently-malformed identifiers that flow into expand-time SQL generation | mitigate | B1 + B2 port to `find_identifier_end` (identifier-aware tokenisation) mirroring TECH-DEBT #24's Phase 67 Plan 02 fix. `is_quoting_balanced` (from 68-01 Task 2) catches unterminated quotes with a structured ParseError. D-08 dotted-path acceptance is a contract extension — no existing fixture relied on the shredding behaviour (verified by RESEARCH §B1/§B2). |
</threat_model>

<verification>
After all three tasks complete:

```bash
just test-all > /tmp/claude/final.log 2>&1
RC=$?
tail -100 /tmp/claude/final.log
[ $RC -eq 0 ] || exit 1

just ci > /tmp/claude/final_ci.log 2>&1
RC=$?
tail -100 /tmp/claude/final_ci.log
[ $RC -eq 0 ] || exit 1

# Belt-and-braces: confirm both new fixtures actually ran (not silently skipped)
grep -c "phase68_quoted_idents_non_additive" /tmp/claude/final.log
grep -c "phase68_quoted_idents_window" /tmp/claude/final.log
# Both MUST be ≥ 1
```

All checks MUST pass.
</verification>

<success_criteria>
- 2 SCOPE.md items (B1, B2) closed.
- `parse_non_additive_dims` and `parse_window_spec` ORDER BY arm both use `find_identifier_end` + the shared `is_quoting_balanced` helper from 68-01.
- D-08 dotted-path acceptance lands in both sites (contract extension verified by new sqllogictest fixtures).
- Two new sqllogictest fixtures registered in `test/sql/TEST_LIST` (D-09 hard gate).
- 8+ new Rust unit tests in `body_parser::tests` pin all four contracts (quoted-whitespace, dotted-path, unterminated-quote, bare regression) for both B1 and B2.
- `just test-all` + `just ci` both exit 0.
- No `--no-verify` used.
- If port body delta exceeds ~50 LOC per site (D-10 threshold), SUMMARY finding documents renegotiation.
</success_criteria>

<output>
Create `.planning/phases/68-pre-tag-cleanup-phase-67-review-pr-35-code-review-follow-ups/68-03-SUMMARY.md` when done.
</output>