From e22f933e02ce7e1881d88f69b8ffb0f28f8dd432 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Tue, 19 May 2026 07:08:13 +0000 Subject: [PATCH] walker: diagnostics-severity model + input_verdict (ADR-0027 step A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `Severity` (Error / Warning, ordered so Error > Warning) and `Diagnostic { severity, span, message }` in `walker::outcome`, plus a `diagnostics` field on `WalkResult` — the schema-aware findings layered on a structurally-valid parse (ADR-0027 §2). `input_verdict(source, schema)` is the validity-indicator entry point: `None` when the input would run clean (and for empty input), `Some(Error)` for a parse failure or unknown command, `Some(Warning)` for the ADR-0026 expression flags. The verdict is the highest severity across the parse outcome and the diagnostics set. `diagnostics` is empty at this step — the schema-existence (ERROR) and expression (WARNING) passes that fill it land next. Covered by `input_verdict` unit tests. --- src/dsl/walker/mod.rs | 81 +++++++++++++++++++++++++++++++++++++++ src/dsl/walker/outcome.rs | 41 ++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/src/dsl/walker/mod.rs b/src/dsl/walker/mod.rs index 8184f60..794d119 100644 --- a/src/dsl/walker/mod.rs +++ b/src/dsl/walker/mod.rs @@ -29,6 +29,7 @@ use crate::dsl::walker::outcome::{ pub use context::ColumnInfo; pub use highlight::highlight_runs; +pub use outcome::{Diagnostic, Severity}; /// Resolve the hint-panel mode at the end of `source` /// (ADR-0024 §HintMode-per-node, §Phase D §typed-value-slots). @@ -291,6 +292,44 @@ pub fn completion_probe( } } +/// The validity-indicator verdict for `source` (ADR-0027 §3). +/// +/// `None` — the input would run clean (the indicator shows +/// nothing); empty / whitespace-only input is also `None`. +/// `Some(Error)` — pressing Enter now fails (a structural +/// parse failure, or a schema-existence diagnostic). +/// `Some(Warning)` — it runs, but is very likely not intended +/// (the ADR-0026 expression flags). +/// +/// The verdict is the highest severity across the parse +/// outcome and the `diagnostics` set (ADR-0027 §2). +#[must_use] +pub fn input_verdict( + source: &str, + schema: Option<&crate::completion::SchemaCache>, +) -> Option { + use outcome::Severity; + if source.trim().is_empty() { + return None; + } + let mut ctx = schema.map_or_else( + context::WalkContext::new, + context::WalkContext::with_schema, + ); + let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx); + let Some(result) = result else { + // The first token is not a registered command word — + // typing this and pressing Enter fails. + return Some(Severity::Error); + }; + let outcome_severity = match result.outcome { + outcome::WalkOutcome::Match { .. } => None, + _ => Some(Severity::Error), + }; + let diag_severity = result.diagnostics.iter().map(|d| d.severity).max(); + outcome_severity.into_iter().chain(diag_severity).max() +} + /// What the grammar would accept at the end of `source` /// (ADR-0024 §architecture, Phase F walker-driven completion). /// @@ -570,6 +609,9 @@ pub fn walk<'a>( matched_path: path, per_byte_class: per_byte, tail_expected, + // Filled by the schema-existence and expression passes + // (ADR-0027 §2); empty here at the structural layer. + diagnostics: Vec::new(), }; (Some(result), cmd) } @@ -1236,6 +1278,45 @@ mod tests { )); } + // ---- input_verdict (ADR-0027 §3) -------------------------- + + #[test] + fn input_verdict_clean_command_is_none() { + assert_eq!(super::input_verdict("quit", None), None); + assert_eq!(super::input_verdict("show table Customers", None), None); + } + + #[test] + fn input_verdict_empty_input_is_none() { + assert_eq!(super::input_verdict("", None), None); + assert_eq!(super::input_verdict(" ", None), None); + } + + #[test] + fn input_verdict_incomplete_command_is_error() { + assert_eq!( + super::input_verdict("create table", None), + Some(super::Severity::Error), + ); + } + + #[test] + fn input_verdict_unknown_command_is_error() { + assert_eq!( + super::input_verdict("frobnicate the gizmo", None), + Some(super::Severity::Error), + ); + } + + #[test] + fn input_verdict_mismatched_token_is_error() { + // `quit` takes no argument — trailing junk fails. + assert_eq!( + super::input_verdict("quit now", None), + Some(super::Severity::Error), + ); + } + #[test] fn walker_parses_insert_with_explicit_column_list() { assert_eq!( diff --git a/src/dsl/walker/outcome.rs b/src/dsl/walker/outcome.rs index 7d30b6a..1192134 100644 --- a/src/dsl/walker/outcome.rs +++ b/src/dsl/walker/outcome.rs @@ -159,11 +159,52 @@ pub struct ByteClass { pub class: HighlightClass, } +/// Severity of a pre-submit [`Diagnostic`] (ADR-0027 §1). +/// +/// Ordered so `Error > Warning` — taking the `max` across a +/// diagnostic set gives the validity indicator's verdict. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Severity { + /// Valid and runnable, but very likely not what the user + /// intends — a type-mismatched comparison, `= NULL` + /// (ADR-0026 §7). + Warning, + /// Known to fail if submitted now — a parse error, or a + /// reference to a table / column that does not exist. + Error, +} + +/// A problem the walker found in the input *before* submission +/// (ADR-0027 §2). The severity drives the validity indicator; +/// the span drives highlighting; the message is the hint text. +/// +/// Note: the parse outcome (`Incomplete` / `Mismatch` / +/// `ValidationFailed`) is *not* recorded as a `Diagnostic` — +/// the indicator reads it from `WalkOutcome` directly +/// (ADR-0027 §2, "the highest severity across the outcome and +/// the diagnostics"). `diagnostics` carries the schema-aware +/// findings layered on top of a structurally-valid parse. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Diagnostic { + pub severity: Severity, + /// Byte range in the source the diagnostic refers to. + pub span: (usize, usize), + /// Ready-to-show message — already catalog-translated by + /// whichever pass produced the diagnostic. + pub message: String, +} + #[derive(Debug, Clone)] pub struct WalkResult { pub outcome: WalkOutcome, pub matched_path: MatchedPath, pub per_byte_class: Vec, + /// Schema-aware pre-submit findings layered on a + /// structurally-valid parse (ADR-0027): unknown table / + /// column references (ERROR), type-mismatched WHERE + /// comparisons and `= NULL` (WARNING). Empty when the + /// input does not parse — those are the outcome's job. + pub diagnostics: Vec, /// Optional-Optional expectations the walker could have /// accepted but didn't because the outer shape ran out at a /// node boundary (ADR-0024 §architecture, round-5 follow-up).