# Session handoff — 2026-05-27 (44)

Forty-fourth handover. This session **completed ADR-0036** — value handling
for advanced-mode SQL DML — by shipping Phase 2, Phase 3a, and Phase 3b
(picking up from handoff-43, which had landed Phase 1), then closing the
one Phase-1 carryover gap. ADR-0036 is now fully implemented; the next
session starts from a clean slate on it.

## §1. State at handoff

**Branch:** `main`. **HEAD `d987171`.** **Tests: 1948 passing, 0 failing,
0 skipped, 1 ignored** (the `friendly/mod.rs` ` ```ignore ` doctest).
**Clippy:** clean (`cargo clippy --all-targets -- -D warnings`).

This session's commits (newest first):

```
d987171 fix: ADR-0036 — name the offending value for natural-order SQL INSERTs
8906661 feat: ADR-0036 Phase 3b — live typed-slot hints + highlighting for INSERT VALUES
49ea03b feat: ADR-0036 Phase 3a — live typed-slot hints + highlighting for SQL SET values
8c3b13b feat: ADR-0036 Phase 2 — validate advanced-mode UPDATE SET literals + retain the value
```

## §2. What shipped — ADR-0036 complete

Read **`docs/adr/0036-typed-dml-values-vs-verbatim.md`** in full, and
especially **Amendment 1**, which is now the authoritative record of the
Phase 3 mechanism (it supersedes the original §5 sketch — see §3 below).

The four ADR-0036 phases, end to end:

- **Phase 1 (handoff-43) — `INSERT … VALUES` literal validation.**
  Capture each literal value position at parse onto
  `Command::SqlInsert.literal_rows`; `do_sql_insert` validates them
  against column types (shared `impl_value_for`) before the verbatim
  insert; the error enricher names the offending value. No grammar
  change.
- **Phase 2 (`8c3b13b`) — `UPDATE … SET` literal validation.** The same
  capture-at-parse technique on the SET assignment list:
  `capture_set_literals` (`src/dsl/grammar/data.rs`) classifies each
  top-level `SET col = <rhs>` into `(col, Some(Value))` (a bare literal,
  incl. a signed number) or `(col, None)` (an expression), using paren
  depth so a comma in a function call or a `where` in a subquery isn't
  a boundary, and excluding the trailing top-level `WHERE`.
  `Command::SqlUpdate` gained `set_literals`; `do_sql_update` validates;
  `user_value_for_column` reads them. `WHERE` is deliberately not
  validated.
- **Phase 3a (`49ea03b`) — live typed-slot hints + highlighting for
  `SET col = <literal>`.** The `SET` column ident now sets
  `writes_column: true` (so `current_column` / `pending_value_column`
  are populated per assignment); the RHS became the shared
  `shared::SET_VALUE` slot — a **boundary-aware lookahead** that routes a
  *lone literal* to the column-typed slot (live hint + numeric-shape
  highlight, shared with the DSL) and any expression to `sql_expr`.
  Covers `sql_update`'s assignment list and `sql_insert`'s
  `ON CONFLICT … DO UPDATE SET`.
- **Phase 3b (`8906661`) — live per-position typed slots for
  `INSERT … VALUES (…)`** (single/multi-row, Form A and Form B). The
  hard half — see §3.

### Phase-1 carryover closed (`d987171`)

A no-column-list (natural-order / Form B) SQL `INSERT` that hit a
UNIQUE/CHECK violation used to degrade to the neutral "that value"
because `user_value_for_column` only resolved the offending value for the
explicit-column-list form. `user_value_for_column_with_schema`
(`src/runtime.rs`) now maps each `VALUES` position to the schema's columns
in **declaration order — all columns** (advanced-mode Form B auto-fills
nothing, so every column has a value), single-row only (multi-row stays
ambiguous → neutral). This was the only tracked ADR-0036 follow-up; it is
done.

## §3. Phase 3 design — the load-bearing details

Two design forks were escalated to and decided by the user; both are
recorded in **ADR-0036 Amendment 1**. Future work on the grammar must
respect them.

### The mechanism is a boundary-aware lookahead, NOT a naive `Choice`

ADR-0036 §5 originally sketched Phase 3 as `Choice(typed-literal-slot,
sql_expr)` at each value position. **That is wrong and would regress valid
SQL.** The walker's `Node::Choice` is first-match-wins with no
cross-branch backtrack, so a typed slot would greedily match the leading
`1` of `1 + 2` and commit, leaving `+ 2` dangling — breaking, e.g., the
existing `values (1, 1 + 2)` test. The correction (Amendment 1): a
**lookahead** peeks the whole value position and routes a literal to the
typed slot *only when the literal fills the position* (up to the next
`,` / `)` / `;` / `where` / `returning` / end); everything else goes to
`sql_expr`. `shared::SET_VALUE` (`set_value_node` + `set_rhs_is_lone_literal`)
is that primitive; it's reused by both 3a and 3b. Empty positions route
to the typed slot too, so the hint shows from the moment the cursor lands.

### Phase 3b's new `Node::SetColumn` primitive + arity reconciliation

`INSERT … VALUES` positions are **positional** (no per-position column
ident) and **multi-row**, so the user approved adding a grammar primitive
(the "full parity" option):

- **`Node::SetColumn(&'static TableColumn)`** (`src/dsl/grammar/mod.rs`,
  walker arm in `src/dsl/walker/driver.rs`) — a **zero-width** node that
  sets `current_column` + `pending_value_column`, like an
  `Ident { writes_column: true }` but without consuming input. One enum
  variant, one walker arm (only one exhaustive `Node` match site exists).
- The factory **`sql_insert::sql_value_list`** emits `SetColumn(colᵢ)`
  then `SET_VALUE` per position. Column mapping mirrors `do_sql_insert`:
  **Form A → listed columns in user order; Form B → ALL columns in
  declaration order.**
- **Auto-fill clarification (corrects a loose statement made while
  scoping):** advanced mode **does** auto-fill an *omitted* `shortid` in
  **Form A** (`plan_shortid_autofill`) — that column has no `VALUES`
  position and is correctly absent from the mapping; it does **not**
  auto-fill `serial` (the **X4** gap); and **Form B auto-fills nothing**
  (the function returns early on an empty column list).
- **Arity reconciliation (the subtle part).** A fixed-length typed `Seq`
  would *reject* a wrong-arity tuple, suppressing the friendly per-tuple
  `insert_arity_mismatch` diagnostic (ADR-0033 §8.1), which is a
  post-walk pass over the matched path and so needs the tuple to be
  accepted. So the tuple value list is an **arity-gating lookahead**
  (`tuple_value_list` + `count_tuple_values`): a *closed* tuple uses the
  typed `Seq` only on an exact value/column match; an *open* (mid-typing)
  tuple uses it while `count <= columns` (so the hint shows from `(`
  onward); every other case falls back to the type-blind
  `Repeated(sql_expr)`, leaving §8.1 to fire unchanged. Correct-arity
  tuples (the common case) get full live typed feedback — including a
  wrong-*kind* lone literal like `('text')` into an `int` column — while
  wrong-arity tuples keep the friendly arity message.

**Known limitation (all phases, matches the DSL):** `date` / `shortid` /
`datetime` *format* is still not validated at parse — those slots accept
any quoted string; format is checked at bind/execution time. The live
highlight catches *numeric-shape* mismatches (`int`/`decimal`/`bool`); the
column-type *hint* shows for every type.

## §4. Where the code lives (ADR-0036 surface)

- **Parse-time capture / typed slots:** `src/dsl/grammar/data.rs`
  (`capture_literal_rows`, `capture_set_literals`, `build_sql_insert`,
  `build_sql_update`); `src/dsl/grammar/shared.rs` (`SET_VALUE`,
  `set_value_node`, `set_rhs_is_lone_literal`, `next_is_set_boundary`,
  the typed `*_SLOT`s, `current_column_value`);
  `src/dsl/grammar/sql_insert.rs` (`sql_value_list`, `tuple_value_list`,
  `count_tuple_values`, `target_value_columns`, `fallback_value_list`,
  `VALUE_TUPLE`); `src/dsl/grammar/sql_update.rs` (`ASSIGN_COLUMN`,
  `SET_VALUE` RHS).
- **The primitive:** `Node::SetColumn` in `src/dsl/grammar/mod.rs`; its
  arm in `src/dsl/walker/driver.rs`.
- **Worker validation:** `src/db.rs` `do_sql_insert` / `do_sql_update`
  (validate the captured literals via `impl_value_for`).
- **Error enrichment:** `src/runtime.rs` `user_value_for_column` /
  `user_value_for_column_with_schema`.
- **Tests:** `tests/sql_update.rs` (Phase 2 + 3a, incl. the advanced-mode
  typing-surface helpers `schema_cache` + `ambient_hint_in_mode` /
  `classify_input_with_schema_in_mode` against `Mode::Advanced`);
  `tests/sql_insert.rs` (Phase 3b, `vschema` + `prose_at`);
  `tests/friendly_enrichment.rs` (offending-value enrichment for
  SqlUpdate + natural-order SqlInsert).

## §5. Open / tracked work (none from ADR-0036)

- **X4 — `serial` non-PK auto-fill difference (possible bug, the most
  concrete next item).** `requirements.md` Cross-cutting. Simple-mode
  `do_insert` auto-fills an omitted non-PK `serial` with `MAX(col)+1`;
  advanced-mode (`plan_shortid_autofill`) auto-fills only `shortid`,
  never non-PK `serial`. Likely unintended. Decide whether advanced mode
  should match (probably yes) and align ADR-0018. Untouched by ADR-0036
  (surgical by design).
- **X5 — framework cohesion / restructuring (strategic).**
  `requirements.md` Cross-cutting. The grammar/execution framework grew
  organically; a *descriptive* ADR (map what exists + the
  share-a-mechanic-not-a-command reuse-boundary rule) is the likely
  cheaper first step before any restructure. ADR-0036 is a worked example
  of the rule in practice. Not scheduled.
- **Long-tail (carried from handoff-39 §8 / handoff-43 §5):**
  app-lifecycle runtime-failure journalling; M4 execution-time mode
  side-channel; the `blob` value literal (belongs in the literal layer);
  CI/TT5 (test infra exists, no workflow yet); the DSL→SQL teaching echo
  (ADR-0030 Phase 5); H1 full SQL→English error translation;
  H1a syntax-help in parse errors; tutorial/lesson system (needs its own
  ADR); V3/V4 (ER diagram export, session log + Markdown export); the I-
  series input UX (readline shortcuts, multi-line, tab completion, syntax
  highlighting).

## §6. Process pins (reinforced this session)

- **Confirm every commit** (propose the message, wait). No AI attribution.
- **Escalate grammar-touching / architectural forks; don't decide for the
  user.** This session escalated two Phase-3 forks (the lookahead
  mechanism correction; the 3a/3b split and the 3b full-parity-vs-
  pragmatic choice) — both materially changed the work and were the
  user's call. The arity-diagnostic conflict surfaced *during* 3b
  implementation (an exploration gap) and was reconciled within the
  approved option without regressing §8.1 — flagged to the user rather
  than absorbed silently.
- **`/runda`-style verification at planning exit earns its keep**, and so
  does re-checking assumptions against the code: the "advanced mode
  doesn't auto-fill serial/shortid" claim was wrong for `shortid`, caught
  by the user and confirmed against `plan_shortid_autofill`.
- **Keep docs lockstep** — ADR body + Amendment + README index move with
  the code in the same change.
- **ADRs aren't re-litigated casually** — when a decision needs to change
  (the Phase 3 mechanism), write an Amendment. Amendment 1 is the live
  Phase 3 record.

## §7. How to take over

1. **Read, in order:** this file → `CLAUDE.md` →
   `docs/adr/0036-typed-dml-values-vs-verbatim.md` (esp. **Amendment 1**)
   → `docs/requirements.md` X4/X5.
2. **Baseline:** `cargo test` (1948 / 0 / 0 / 1 ignored) +
   `cargo clippy --all-targets -- -D warnings` (clean).
3. Pick up **X4** (the most concrete tracked bug) or a long-tail item
   with the user. For X4, the relevant code is `db.rs` `do_insert`
   (simple-mode `MAX+1` serial fill) vs `plan_shortid_autofill`
   (advanced-mode shortid-only fill), and ADR-0018 is the decision to
   align.
