ADR-0024 Phase D: include column name in value-slot hint prose

User-facing improvement: typing into a value slot now surfaces
the column name in the hint. The hint at `insert into Customers
values (` (first column id:int) reads "for `id`: Type an
integer (e.g. 42, -7) or null" instead of the generic
"Type an integer …" prose. After `1, ` the panel updates to
the second column ("for `Name`: Type a quoted string …"). The
same applies to `update T set Email=` and `delete from T where
ts=` — the catalog wrapper threads the column name through.

Implementation:

**`Node::TypedValueSlot.column_name: Option<&'static str>`**
(new field, `src/dsl/grammar/mod.rs`). When `Some`, walker
writes `WalkContext::pending_value_column` on entry; clears
along with `pending_value_type` on inner success.

**Walker driver writes both names** (`src/dsl/walker/driver.rs`):
- `Node::TypedValueSlot` dispatch reads `column_name` and
  populates `pending_value_column`.
- `Ident { writes_column: true }` dispatch also writes
  `pending_value_column` (using the schema-canonical name when
  available, falling back to the user's spelling) so update
  set / where positions surface the column name.

**Shared sub-grammars** (`src/dsl/grammar/shared.rs`):
- New `slot_for_column(ty, name)` builds a `TypedValueSlot`
  with the embedded leaked column name. Used by
  `column_value_list`.
- New `slot_inner_for_type(ty)` returns just the Choice
  (without TypedValueSlot wrapper) for slot_for_column to
  rebuild.
- `column_value_list` factory now constructs per-column slots
  via `slot_for_column(col.user_type, &col.name)`. Each slot
  leaks its column name string with the same per-walk Box::leak
  pattern the rest of dynamic dispatch uses.

**`WalkContext::pending_value_column: Option<String>`** (new
field, `src/dsl/walker/context.rs`). Pairs with
`pending_value_type` to give the hint resolver both pieces.

**Single-walk hint resolver** (`src/dsl/walker/mod.rs`):
- New `HintResolution { mode: HintMode, column: Option<String> }`
  struct.
- New `hint_resolution_at_input(source, schema) -> Option<
  HintResolution>` runs one walk and reports both pieces. The
  ambient_hint dispatch composes per-column prose from the
  result.
- Existing `hint_mode_at_input` / `hint_mode_at_input_with_schema`
  preserved as thinner wrappers for tests / future callers
  that don't need the column name.

**Catalog wrapper** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- New `hint.value_slot_for_column: "for `{column}`: {detail}"`
  prefixes the per-type prose with the actual column name when
  the walker has it bound. Schemaless fallback continues to use
  the generic value-literal prose with no column prefix.

**ambient_hint composes** (`src/input_render.rs`): consults
`hint_resolution_at_input`; when `column` is `Some`, wraps the
type prose through `hint.value_slot_for_column`; otherwise
emits the bare type prose.

Tests (846 total, 0 failing):
- 4 new input_render tests assert column names appear in the
  prose at insert/update/where positions plus the
  second-insert-value position (proves column tracking advances
  with comma).
- All existing tests pass unchanged — the column-name addition
  is layered on top of the type-only prose path.

Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-15 18:33:52 +00:00
parent 82955679ca
commit c485189da8
8 changed files with 304 additions and 23 deletions
+25 -5
View File
@@ -130,17 +130,27 @@ pub fn walk_node(
let resolved: &'static Node = Box::leak(Box::new(factory(ctx)));
walk_node(source, pos, resolved, ctx, path, per_byte)
}
Node::TypedValueSlot { ty, inner } => {
Node::TypedValueSlot {
ty,
column_name,
inner,
} => {
// ADR-0024 §Phase D §typed-value-slots. Tag the
// pending column type so the hint resolver can emit
// per-type prose at empty prefix. Clear on
// successful inner match — positions BETWEEN typed
// slots (post-comma, between values) don't carry a
// stale hint type.
// per-type prose at empty prefix. If a column name
// is embedded (insert column_value_list path), tag
// that too so the hint can mention the column by
// name. Clear on successful inner match — positions
// BETWEEN typed slots (post-comma, between values)
// don't carry stale hint state.
ctx.pending_value_type = Some(*ty);
if let Some(name) = column_name {
ctx.pending_value_column = Some((*name).to_string());
}
let result = walk_node(source, pos, inner, ctx, path, per_byte);
if matches!(result, NodeWalkResult::Matched { .. }) {
ctx.pending_value_type = None;
ctx.pending_value_column = None;
}
result
}
@@ -268,6 +278,16 @@ fn walk_ident(
.find(|c| c.name.eq_ignore_ascii_case(&text))
.cloned()
});
// Surface the column name to the hint resolver too —
// this is the `update <T> set <col>=` / `where <col>=`
// path. The matching column's canonical name (from the
// schema) wins over the user's spelling so the hint
// mirrors what's in the schema.
ctx.pending_value_column = ctx
.current_column
.as_ref()
.map(|c| c.name.clone())
.or_else(|| Some(text.clone()));
}
path.push(MatchedItem {
kind: MatchedKind::Ident { role },