walker: diagnostics-severity model + input_verdict (ADR-0027 step A)

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.
This commit is contained in:
claude@clouddev1
2026-05-19 07:08:13 +00:00
parent dfd3c51643
commit e22f933e02
2 changed files with 122 additions and 0 deletions
+81
View File
@@ -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<outcome::Severity> {
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!(
+41
View File
@@ -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<ByteClass>,
/// 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<Diagnostic>,
/// 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).