Skip to main content

achitekfile/
diagnostics.rs

1//! Structured diagnostics for Achitekfile source violations.
2//!
3//! Diagnostics describe violations found while parsing or analyzing
4//! Achitekfile source. They are intended for user-facing tooling such as
5//! language servers, command-line validators, formatters, and documentation
6//! generators.
7//!
8//! A diagnostic is different from a fatal Rust error. Invalid Achitekfile source
9//! is normal input for editor and validation workflows, so callers should be
10//! able to receive a partial analysis result plus every diagnostic that could
11//! be discovered.
12//!
13//! Diagnostic codes are stable identifiers for classes of violations. Message
14//! text and help text may improve over time, but released codes should not be
15//! reused for different meanings.
16//!
17//! # Codes
18//!
19//! Diagnostic codes are distinguished by range:
20//!
21//! - `ACH0000`-`ACH0999`: syntax and parse diagnostics
22//! - `ACH1000`-`ACH1999`: single-file semantic diagnostics
23//! - `ACH2000`-`ACH2999`: dependency graph diagnostics
24//! - `ACH3000`-`ACH3999`: validation rule diagnostics
25//!
26//! | violation | kind | severity | code |
27//! | --- | --- | --- | --- |
28//! | Missing blueprint block | [syntax] | [error] | [ACH0000] |
29//! | Multiple blueprint blocks | [syntax] | [error] | [ACH0001] |
30//! | Prompt before blueprint | [syntax] | [error] | [ACH0002] |
31//! | Unknown top-level item | [syntax] | [error] | [ACH0003] |
32//! | Unknown blueprint attribute | [syntax] | [error] | [ACH0004] |
33//! | Unknown prompt attribute | [syntax] | [error] | [ACH0005] |
34//! | Unknown validate attribute | [syntax] | [error] | [ACH0006] |
35//! | Unknown prompt type | [syntax] | [error] | [ACH0007] |
36//! | Invalid boolean literal | [syntax] | [error] | [ACH0008] |
37//! | Unterminated string | [syntax] | [error] | [ACH0009] |
38//! | Invalid escape sequence | [syntax] | [error] | [ACH0010] |
39//! | Invalid dependency expression | [syntax] | [error] | [ACH0011] |
40//! | Unknown dependency method | [syntax] | [error] | [ACH0012] |
41//! | Invalid identifier | [syntax] | [error] | [ACH0013] |
42//! | Invalid integer | [syntax] | [error] | [ACH0014] |
43//! | Malformed array | [syntax] | [error] | [ACH0015] |
44//! | Missing prompt name | [syntax] | [error] | [ACH0016] |
45//! | Missing attribute value | [syntax] | [error] | [ACH0017] |
46//! | Missing blueprint version | [semantic] | [error] | [ACH1000] |
47//! | Missing blueprint name | [semantic] | [error] | [ACH1001] |
48//! | Empty blueprint name | [semantic] | [error] | [ACH1002] |
49//! | Empty blueprint version | [semantic] | [error] | [ACH1003] |
50//! | Duplicate blueprint attribute | [semantic] | [error] | [ACH1004] |
51//! | Missing prompt type | [semantic] | [error] | [ACH1005] |
52//! | Empty prompt name | [semantic] | [error] | [ACH1006] |
53//! | Duplicate prompt name | [semantic] | [error] | [ACH1007] |
54//! | Duplicate prompt attribute | [semantic] | [error] | [ACH1008] |
55//! | Duplicate validate attribute | [semantic] | [error] | [ACH1009] |
56//! | Choices on non-choice prompt | [semantic] | [error] | [ACH1010] |
57//! | Missing choices for select | [semantic] | [error] | [ACH1011] |
58//! | Missing choices for multiselect | [semantic] | [error] | [ACH1012] |
59//! | Empty choices list | [semantic] | [error] | [ACH1013] |
60//! | Duplicate choice | [semantic] | [warning] | [ACH1014] |
61//! | Non-string choice | [semantic] | [error] | [ACH1015] |
62//! | Default type mismatch | [semantic] | [error] | [ACH1016] |
63//! | Select default not in choices | [semantic] | [error] | [ACH1017] |
64//! | Multiselect default must be array | [semantic] | [error] | [ACH1018] |
65//! | Multiselect default contains unknown choice | [semantic] | [error] | [ACH1019] |
66//! | Required false with no default | [semantic] | [hint] | [ACH1020] |
67//! | Duplicate validate block | [semantic] | [error] | [ACH1021] |
68//! | Invalid blueprint version | [semantic] | [error] | [ACH1022] |
69//! | Invalid minimum Achitek version | [semantic] | [error] | [ACH1023] |
70//! | Dependency references unknown prompt | [dependency] | [error] | [ACH2000] |
71//! | Dependency references itself | [dependency] | [error] | [ACH2001] |
72//! | Dependency cycle | [dependency] | [error] | [ACH2002] |
73//! | Dependency type mismatch | [dependency] | [error] | [ACH2003] |
74//! | Contains on non-multiselect prompt | [dependency] | [error] | [ACH2004] |
75//! | Contains unknown choice | [dependency] | [error] | [ACH2005] |
76//! | String validation on non-string prompt | [validation] | [error] | [ACH3000] |
77//! | Selection validation on non-multiselect prompt | [validation] | [error] | [ACH3001] |
78//! | Invalid length bounds | [validation] | [error] | [ACH3002] |
79//! | Invalid selection bounds | [validation] | [error] | [ACH3003] |
80//! | Invalid regex | [validation] | [error] | [ACH3004] |
81//!
82//! ## Code stability
83//!
84//! Diagnostic codes are part of this crate's public API.
85//!
86//! - Released codes keep their meaning across compatible releases.
87//! - Do not reuse a removed code for a different diagnostic.
88//! - Prefer adding a new code when a diagnostic splits into multiple cases.
89//! - Message and help text may change over time.
90//! - Tests and downstream tools should rely on codes, not exact prose.
91//! - Code severity should remain stable unless changing it is intentional and
92//!   documented in release notes.
93//!
94//! [syntax]: DiagnosticKind::Syntax
95//! [semantic]: DiagnosticKind::Semantic
96//! [dependency]: DiagnosticKind::Dependency
97//! [validation]: DiagnosticKind::Validation
98//!
99//! [error]: Severity::Error
100//! [warning]: Severity::Warning
101//! [hint]: Severity::Hint
102//!
103//! [ACH0000]: DiagnosticCode::MissingBlueprintBlock
104//! [ACH0001]: DiagnosticCode::MultipleBlueprintBlocks
105//! [ACH0002]: DiagnosticCode::PromptBeforeBlueprint
106//! [ACH0003]: DiagnosticCode::UnknownTopLevelItem
107//! [ACH0004]: DiagnosticCode::UnknownBlueprintAttribute
108//! [ACH0005]: DiagnosticCode::UnknownPromptAttribute
109//! [ACH0006]: DiagnosticCode::UnknownValidateAttribute
110//! [ACH0007]: DiagnosticCode::UnknownPromptType
111//! [ACH0008]: DiagnosticCode::InvalidBooleanLiteral
112//! [ACH0009]: DiagnosticCode::UnterminatedString
113//! [ACH0010]: DiagnosticCode::InvalidEscapeSequence
114//! [ACH0011]: DiagnosticCode::InvalidDependencyExpression
115//! [ACH0012]: DiagnosticCode::UnknownDependencyMethod
116//! [ACH0013]: DiagnosticCode::InvalidIdentifier
117//! [ACH0014]: DiagnosticCode::InvalidInteger
118//! [ACH0015]: DiagnosticCode::MalformedArray
119//! [ACH0016]: DiagnosticCode::MissingPromptName
120//! [ACH0017]: DiagnosticCode::MissingAttributeValue
121//! [ACH1000]: DiagnosticCode::MissingBlueprintVersion
122//! [ACH1001]: DiagnosticCode::MissingBlueprintName
123//! [ACH1002]: DiagnosticCode::EmptyBlueprintName
124//! [ACH1003]: DiagnosticCode::EmptyBlueprintVersion
125//! [ACH1004]: DiagnosticCode::DuplicateBlueprintAttribute
126//! [ACH1005]: DiagnosticCode::MissingPromptType
127//! [ACH1006]: DiagnosticCode::EmptyPromptName
128//! [ACH1007]: DiagnosticCode::DuplicatePromptName
129//! [ACH1008]: DiagnosticCode::DuplicatePromptAttribute
130//! [ACH1009]: DiagnosticCode::DuplicateValidateAttribute
131//! [ACH1010]: DiagnosticCode::ChoicesOnNonChoicePrompt
132//! [ACH1011]: DiagnosticCode::MissingChoicesForSelect
133//! [ACH1012]: DiagnosticCode::MissingChoicesForMultiselect
134//! [ACH1013]: DiagnosticCode::EmptyChoicesList
135//! [ACH1014]: DiagnosticCode::DuplicateChoice
136//! [ACH1015]: DiagnosticCode::NonStringChoice
137//! [ACH1016]: DiagnosticCode::DefaultTypeMismatch
138//! [ACH1017]: DiagnosticCode::SelectDefaultNotInChoices
139//! [ACH1018]: DiagnosticCode::MultiselectDefaultMustBeArray
140//! [ACH1019]: DiagnosticCode::MultiselectDefaultContainsUnknownChoice
141//! [ACH1020]: DiagnosticCode::RequiredFalseWithNoDefault
142//! [ACH1021]: DiagnosticCode::DuplicateValidateBlock
143//! [ACH1022]: DiagnosticCode::InvalidBlueprintVersion
144//! [ACH1023]: DiagnosticCode::InvalidMinimumAchitekVersion
145//! [ACH2000]: DiagnosticCode::UnknownDependencyReference
146//! [ACH2001]: DiagnosticCode::SelfDependency
147//! [ACH2002]: DiagnosticCode::DependencyCycle
148//! [ACH2003]: DiagnosticCode::DependencyTypeMismatch
149//! [ACH2004]: DiagnosticCode::ContainsOnNonMultiselectPrompt
150//! [ACH2005]: DiagnosticCode::ContainsUnknownChoice
151//! [ACH3000]: DiagnosticCode::StringValidationOnNonStringPrompt
152//! [ACH3001]: DiagnosticCode::SelectionValidationOnNonMultiselectPrompt
153//! [ACH3002]: DiagnosticCode::InvalidLengthBounds
154//! [ACH3003]: DiagnosticCode::InvalidSelectionBounds
155//! [ACH3004]: DiagnosticCode::InvalidRegex
156
157/// A user-facing issue found in Achitekfile source.
158///
159/// Diagnostics carry stable machine-readable metadata that downstream tools can
160/// map into their own reporting formats. For example, `achitek-ls` can convert
161/// this type into an LSP diagnostic without defining its own Achitekfile
162/// diagnostic codes.
163///
164/// # Examples
165///
166/// ```
167/// let source = r#"
168/// blueprint {
169///   version = "1.0.0"
170///   name = "web-app"
171/// }
172///
173/// prompt "project_name" {
174///   help = "Project name"
175/// }
176/// "#;
177///
178/// let analysis = achitekfile::analyze(source)?;
179/// let diagnostic = &analysis.diagnostics()[0];
180///
181/// assert_eq!(diagnostic.code(), achitekfile::DiagnosticCode::MissingPromptType);
182/// assert_eq!(diagnostic.severity(), achitekfile::Severity::Error);
183/// assert_eq!(diagnostic.kind(), achitekfile::DiagnosticKind::Semantic);
184/// # Ok::<(), Box<dyn std::error::Error>>(())
185/// ```
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
188pub struct Diagnostic {
189    /// Stable identifier for this class of diagnostic.
190    code: DiagnosticCode,
191    // /// Broad category that produced the diagnostic.
192    // kind: DiagnosticKind,
193    /// How strongly tooling should surface the diagnostic.
194    severity: Severity,
195    /// Informational message about the diagnostic.
196    message: String,
197    /// Help message to assist in remediating the diagnostic.
198    help: Option<String>,
199    /// The source span where something appears in the achitekfile
200    range: TextRange,
201}
202impl Diagnostic {
203    /// Creates a diagnostic from a code and source range.
204    pub(crate) fn new(code: DiagnosticCode, range: TextRange) -> Self {
205        Self {
206            code,
207            severity: code.severity(),
208            message: code.message().to_owned(),
209            help: code.help().map(str::to_owned),
210            range,
211        }
212    }
213
214    /// Creates a diagnostic with custom message text from a code and source
215    /// range.
216    pub(crate) fn with_message(
217        code: DiagnosticCode,
218        range: TextRange,
219        message: impl Into<String>,
220    ) -> Self {
221        Self {
222            code,
223            severity: code.severity(),
224            message: message.into(),
225            help: code.help().map(str::to_owned),
226            range,
227        }
228    }
229
230    /// Returns the stable diagnostic code.
231    pub fn code(&self) -> DiagnosticCode {
232        self.code
233    }
234
235    /// Returns how strongly tooling should surface this diagnostic.
236    pub fn severity(&self) -> Severity {
237        self.severity
238    }
239
240    /// Returns the user-facing diagnostic message.
241    pub fn message(&self) -> &str {
242        &self.message
243    }
244
245    /// Returns optional remediation guidance for this diagnostic.
246    pub fn help(&self) -> Option<&str> {
247        self.help.as_deref()
248    }
249
250    /// Returns the source range associated with this diagnostic.
251    pub fn range(&self) -> TextRange {
252        self.range
253    }
254
255    /// Returns the broad analysis layer that produced this diagnostic.
256    pub fn kind(&self) -> DiagnosticKind {
257        self.code.kind()
258    }
259}
260
261/// Broad category for an Achitekfile diagnostic.
262///
263/// The kind describes which analysis layer produced a diagnostic. It is useful
264/// for grouping diagnostics in docs and tests, while [`DiagnosticCode`] remains
265/// the stable identifier for a specific violation.
266///
267/// See [`Diagnostic`] for an example of reading a diagnostic kind from analysis
268/// output.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
270#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
271pub enum DiagnosticKind {
272    /// A syntax or parse violation in the source text.
273    Syntax,
274    /// A semantic violation in syntactically valid Achitekfile source.
275    Semantic,
276    /// A dependency graph violation between prompt declarations.
277    Dependency,
278    /// A validation rule violation on a prompt declaration.
279    Validation,
280}
281
282/// Stable identifiers for Achitekfile diagnostics.
283///
284/// Codes are part of the public diagnostic contract for downstream tools. Once
285/// released, a code should keep the same meaning. Prefer adding a new code over
286/// reusing or renumbering an existing one.
287///
288/// See [`Diagnostic`] for an example of matching on a stable diagnostic code.
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
290#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
291pub enum DiagnosticCode {
292    /// `ACH0000`: the file does not contain the required `blueprint` block.
293    MissingBlueprintBlock,
294    /// `ACH0001`: the file contains more than one `blueprint` block.
295    MultipleBlueprintBlocks,
296    /// `ACH0002`: a `prompt` block appears before the required `blueprint` block.
297    PromptBeforeBlueprint,
298    /// `ACH0003`: an unsupported item appears at the top level of the file.
299    UnknownTopLevelItem,
300    /// `ACH0004`: a `blueprint` block contains an unsupported attribute.
301    UnknownBlueprintAttribute,
302    /// `ACH0005`: a `prompt` block contains an unsupported attribute.
303    UnknownPromptAttribute,
304    /// `ACH0006`: a `validate` block contains an unsupported attribute.
305    UnknownValidateAttribute,
306    /// `ACH0007`: a `type` attribute uses an unsupported prompt type.
307    UnknownPromptType,
308    /// `ACH0008`: a boolean value is not `true` or `false`.
309    InvalidBooleanLiteral,
310    /// `ACH0009`: a string literal is missing its closing quote.
311    UnterminatedString,
312    /// `ACH0010`: a string literal contains an unsupported escape sequence.
313    InvalidEscapeSequence,
314    /// `ACH0011`: a `depends_on` attribute contains an invalid dependency expression.
315    InvalidDependencyExpression,
316    /// `ACH0012`: a dependency method call uses an unsupported method name.
317    UnknownDependencyMethod,
318    /// `ACH0013`: an identifier does not match Achitekfile identifier syntax.
319    InvalidIdentifier,
320    /// `ACH0014`: an integer literal does not match Achitekfile integer syntax.
321    InvalidInteger,
322    /// `ACH0015`: an array literal is malformed.
323    MalformedArray,
324    /// `ACH0016`: a `prompt` block is missing its required string name.
325    MissingPromptName,
326    /// `ACH0017`: an attribute is missing the value after `=`.
327    MissingAttributeValue,
328    /// `ACH1000`: the `blueprint` block is missing the required `version` attribute.
329    MissingBlueprintVersion,
330    /// `ACH1001`: the `blueprint` block is missing the required `name` attribute.
331    MissingBlueprintName,
332    /// `ACH1002`: the `blueprint.name` attribute is empty.
333    EmptyBlueprintName,
334    /// `ACH1003`: the `blueprint.version` attribute is empty.
335    EmptyBlueprintVersion,
336    /// `ACH1004`: a `blueprint` block contains the same attribute more than once.
337    DuplicateBlueprintAttribute,
338    /// `ACH1005`: a `prompt` block is missing the required `type` attribute.
339    MissingPromptType,
340    /// `ACH1006`: a prompt name is empty.
341    EmptyPromptName,
342    /// `ACH1007`: more than one prompt uses the same name.
343    DuplicatePromptName,
344    /// `ACH1008`: a `prompt` block contains the same attribute more than once.
345    DuplicatePromptAttribute,
346    /// `ACH1009`: a `validate` block contains the same attribute more than once.
347    DuplicateValidateAttribute,
348    /// `ACH1010`: a non-choice prompt declares `choices`.
349    ChoicesOnNonChoicePrompt,
350    /// `ACH1011`: a `select` prompt has no choices.
351    MissingChoicesForSelect,
352    /// `ACH1012`: a `multiselect` prompt has no choices.
353    MissingChoicesForMultiselect,
354    /// `ACH1013`: a `choices` array is empty.
355    EmptyChoicesList,
356    /// `ACH1014`: a `choices` array contains the same choice more than once.
357    DuplicateChoice,
358    /// `ACH1015`: a `choices` array contains a non-string value.
359    NonStringChoice,
360    /// `ACH1016`: a prompt default does not match the prompt type.
361    DefaultTypeMismatch,
362    /// `ACH1017`: a `select` default is not one of the prompt choices.
363    SelectDefaultNotInChoices,
364    /// `ACH1018`: a `multiselect` default is not an array.
365    MultiselectDefaultMustBeArray,
366    /// `ACH1019`: a `multiselect` default contains a value not listed in choices.
367    MultiselectDefaultContainsUnknownChoice,
368    /// `ACH1020`: a prompt explicitly sets `required = false` without a default.
369    RequiredFalseWithNoDefault,
370    /// `ACH1021`: a prompt contains more than one `validate` block.
371    DuplicateValidateBlock,
372    /// `ACH1022`: the `blueprint.version` attribute is not a valid version.
373    InvalidBlueprintVersion,
374    /// `ACH1023`: the `blueprint.min_achitek_version` attribute is not a valid version.
375    InvalidMinimumAchitekVersion,
376    /// `ACH2000`: a dependency references a prompt that does not exist.
377    UnknownDependencyReference,
378    /// `ACH2001`: a prompt depends on itself.
379    SelfDependency,
380    /// `ACH2002`: prompt dependencies contain a cycle.
381    DependencyCycle,
382    /// `ACH2003`: a dependency expression compares incompatible value types.
383    DependencyTypeMismatch,
384    /// `ACH2004`: a dependency uses `contains` on a prompt that is not `multiselect`.
385    ContainsOnNonMultiselectPrompt,
386    /// `ACH2005`: a dependency `contains` argument is not one of the referenced prompt choices.
387    ContainsUnknownChoice,
388    /// `ACH3000`: string validation is used on a non-string prompt.
389    StringValidationOnNonStringPrompt,
390    /// `ACH3001`: selection-count validation is used on a non-`multiselect` prompt.
391    SelectionValidationOnNonMultiselectPrompt,
392    /// `ACH3002`: string length validation bounds are invalid.
393    InvalidLengthBounds,
394    /// `ACH3003`: selection-count validation bounds are invalid.
395    InvalidSelectionBounds,
396    /// `ACH3004`: a `regex` validation rule is not a valid regular expression.
397    InvalidRegex,
398}
399impl DiagnosticCode {
400    /// Returns the broad diagnostic category for this code.
401    pub fn kind(&self) -> DiagnosticKind {
402        match self {
403            Self::MissingBlueprintBlock
404            | Self::MultipleBlueprintBlocks
405            | Self::PromptBeforeBlueprint
406            | Self::UnknownTopLevelItem
407            | Self::UnknownBlueprintAttribute
408            | Self::UnknownPromptAttribute
409            | Self::UnknownValidateAttribute
410            | Self::UnknownPromptType
411            | Self::InvalidBooleanLiteral
412            | Self::UnterminatedString
413            | Self::InvalidEscapeSequence
414            | Self::InvalidDependencyExpression
415            | Self::UnknownDependencyMethod
416            | Self::InvalidIdentifier
417            | Self::InvalidInteger
418            | Self::MalformedArray
419            | Self::MissingPromptName
420            | Self::MissingAttributeValue => DiagnosticKind::Syntax,
421            Self::MissingBlueprintVersion
422            | Self::MissingBlueprintName
423            | Self::EmptyBlueprintName
424            | Self::EmptyBlueprintVersion
425            | Self::DuplicateBlueprintAttribute
426            | Self::MissingPromptType
427            | Self::EmptyPromptName
428            | Self::DuplicatePromptName
429            | Self::DuplicatePromptAttribute
430            | Self::DuplicateValidateAttribute
431            | Self::ChoicesOnNonChoicePrompt
432            | Self::MissingChoicesForSelect
433            | Self::MissingChoicesForMultiselect
434            | Self::EmptyChoicesList
435            | Self::DuplicateChoice
436            | Self::NonStringChoice
437            | Self::DefaultTypeMismatch
438            | Self::SelectDefaultNotInChoices
439            | Self::MultiselectDefaultMustBeArray
440            | Self::MultiselectDefaultContainsUnknownChoice
441            | Self::RequiredFalseWithNoDefault
442            | Self::DuplicateValidateBlock
443            | Self::InvalidBlueprintVersion
444            | Self::InvalidMinimumAchitekVersion => DiagnosticKind::Semantic,
445            Self::UnknownDependencyReference
446            | Self::SelfDependency
447            | Self::DependencyCycle
448            | Self::DependencyTypeMismatch
449            | Self::ContainsOnNonMultiselectPrompt
450            | Self::ContainsUnknownChoice => DiagnosticKind::Dependency,
451            Self::StringValidationOnNonStringPrompt
452            | Self::SelectionValidationOnNonMultiselectPrompt
453            | Self::InvalidLengthBounds
454            | Self::InvalidSelectionBounds
455            | Self::InvalidRegex => DiagnosticKind::Validation,
456        }
457    }
458    /// Returns the severity of the diagnostic code.
459    pub fn severity(&self) -> Severity {
460        match self {
461            Self::DuplicateChoice => Severity::Warning,
462            Self::RequiredFalseWithNoDefault => Severity::Hint,
463            _ => Severity::Error,
464        }
465    }
466    /// Returns the stable machine-readable code.
467    pub fn as_str(&self) -> &'static str {
468        match self {
469            Self::MissingBlueprintBlock => "ACH0000",
470            Self::MultipleBlueprintBlocks => "ACH0001",
471            Self::PromptBeforeBlueprint => "ACH0002",
472            Self::UnknownTopLevelItem => "ACH0003",
473            Self::UnknownBlueprintAttribute => "ACH0004",
474            Self::UnknownPromptAttribute => "ACH0005",
475            Self::UnknownValidateAttribute => "ACH0006",
476            Self::UnknownPromptType => "ACH0007",
477            Self::InvalidBooleanLiteral => "ACH0008",
478            Self::UnterminatedString => "ACH0009",
479            Self::InvalidEscapeSequence => "ACH0010",
480            Self::InvalidDependencyExpression => "ACH0011",
481            Self::UnknownDependencyMethod => "ACH0012",
482            Self::InvalidIdentifier => "ACH0013",
483            Self::InvalidInteger => "ACH0014",
484            Self::MalformedArray => "ACH0015",
485            Self::MissingPromptName => "ACH0016",
486            Self::MissingAttributeValue => "ACH0017",
487            Self::MissingBlueprintVersion => "ACH1000",
488            Self::MissingBlueprintName => "ACH1001",
489            Self::EmptyBlueprintName => "ACH1002",
490            Self::EmptyBlueprintVersion => "ACH1003",
491            Self::DuplicateBlueprintAttribute => "ACH1004",
492            Self::MissingPromptType => "ACH1005",
493            Self::EmptyPromptName => "ACH1006",
494            Self::DuplicatePromptName => "ACH1007",
495            Self::DuplicatePromptAttribute => "ACH1008",
496            Self::DuplicateValidateAttribute => "ACH1009",
497            Self::ChoicesOnNonChoicePrompt => "ACH1010",
498            Self::MissingChoicesForSelect => "ACH1011",
499            Self::MissingChoicesForMultiselect => "ACH1012",
500            Self::EmptyChoicesList => "ACH1013",
501            Self::DuplicateChoice => "ACH1014",
502            Self::NonStringChoice => "ACH1015",
503            Self::DefaultTypeMismatch => "ACH1016",
504            Self::SelectDefaultNotInChoices => "ACH1017",
505            Self::MultiselectDefaultMustBeArray => "ACH1018",
506            Self::MultiselectDefaultContainsUnknownChoice => "ACH1019",
507            Self::RequiredFalseWithNoDefault => "ACH1020",
508            Self::DuplicateValidateBlock => "ACH1021",
509            Self::InvalidBlueprintVersion => "ACH1022",
510            Self::InvalidMinimumAchitekVersion => "ACH1023",
511            Self::UnknownDependencyReference => "ACH2000",
512            Self::SelfDependency => "ACH2001",
513            Self::DependencyCycle => "ACH2002",
514            Self::DependencyTypeMismatch => "ACH2003",
515            Self::ContainsOnNonMultiselectPrompt => "ACH2004",
516            Self::ContainsUnknownChoice => "ACH2005",
517            Self::StringValidationOnNonStringPrompt => "ACH3000",
518            Self::SelectionValidationOnNonMultiselectPrompt => "ACH3001",
519            Self::InvalidLengthBounds => "ACH3002",
520            Self::InvalidSelectionBounds => "ACH3003",
521            Self::InvalidRegex => "ACH3004",
522        }
523    }
524
525    /// Returns the default message for this diagnostic code.
526    pub fn message(&self) -> &'static str {
527        match self {
528            Self::MissingBlueprintBlock => "missing blueprint block",
529            Self::MultipleBlueprintBlocks => "multiple blueprint blocks",
530            Self::PromptBeforeBlueprint => "prompt block appears before blueprint block",
531            Self::UnknownTopLevelItem => "unknown top-level item",
532            Self::UnknownBlueprintAttribute => "unknown blueprint attribute",
533            Self::UnknownPromptAttribute => "unknown prompt attribute",
534            Self::UnknownValidateAttribute => "unknown validate attribute",
535            Self::UnknownPromptType => "unknown prompt type",
536            Self::InvalidBooleanLiteral => "invalid boolean literal",
537            Self::UnterminatedString => "unterminated string literal",
538            Self::InvalidEscapeSequence => "invalid escape sequence",
539            Self::InvalidDependencyExpression => "invalid dependency expression",
540            Self::UnknownDependencyMethod => "unknown dependency method",
541            Self::InvalidIdentifier => "invalid identifier",
542            Self::InvalidInteger => "invalid integer literal",
543            Self::MalformedArray => "malformed array literal",
544            Self::MissingPromptName => "missing prompt name",
545            Self::MissingAttributeValue => "missing attribute value",
546            Self::MissingBlueprintVersion => "missing blueprint version",
547            Self::MissingBlueprintName => "missing blueprint name",
548            Self::EmptyBlueprintName => "empty blueprint name",
549            Self::EmptyBlueprintVersion => "empty blueprint version",
550            Self::DuplicateBlueprintAttribute => "duplicate blueprint attribute",
551            Self::MissingPromptType => "missing prompt type",
552            Self::EmptyPromptName => "empty prompt name",
553            Self::DuplicatePromptName => "duplicate prompt name",
554            Self::DuplicatePromptAttribute => "duplicate prompt attribute",
555            Self::DuplicateValidateAttribute => "duplicate validate attribute",
556            Self::ChoicesOnNonChoicePrompt => "choices on non-choice prompt",
557            Self::MissingChoicesForSelect => "missing choices for select prompt",
558            Self::MissingChoicesForMultiselect => "missing choices for multiselect prompt",
559            Self::EmptyChoicesList => "empty choices list",
560            Self::DuplicateChoice => "duplicate choice",
561            Self::NonStringChoice => "non-string choice",
562            Self::DefaultTypeMismatch => "default type mismatch",
563            Self::SelectDefaultNotInChoices => "select default is not in choices",
564            Self::MultiselectDefaultMustBeArray => "multiselect default must be an array",
565            Self::MultiselectDefaultContainsUnknownChoice => {
566                "multiselect default contains unknown choice"
567            }
568            Self::RequiredFalseWithNoDefault => "required false with no default",
569            Self::DuplicateValidateBlock => "duplicate validate block",
570            Self::InvalidBlueprintVersion => "invalid blueprint version",
571            Self::InvalidMinimumAchitekVersion => "invalid minimum Achitek version",
572            Self::UnknownDependencyReference => "dependency references unknown prompt",
573            Self::SelfDependency => "dependency references itself",
574            Self::DependencyCycle => "dependency cycle",
575            Self::DependencyTypeMismatch => "dependency type mismatch",
576            Self::ContainsOnNonMultiselectPrompt => "contains on non-multiselect prompt",
577            Self::ContainsUnknownChoice => "contains unknown choice",
578            Self::StringValidationOnNonStringPrompt => "string validation on non-string prompt",
579            Self::SelectionValidationOnNonMultiselectPrompt => {
580                "selection validation on non-multiselect prompt"
581            }
582            Self::InvalidLengthBounds => "invalid length bounds",
583            Self::InvalidSelectionBounds => "invalid selection bounds",
584            Self::InvalidRegex => "invalid regex",
585        }
586    }
587
588    /// Returns default help text for this diagnostic code.
589    pub fn help(&self) -> Option<&'static str> {
590        match self {
591            Self::MissingBlueprintBlock => Some("Start the file with a `blueprint { ... }` block."),
592            Self::MultipleBlueprintBlocks => {
593                Some("Keep exactly one `blueprint` block in each Achitekfile.")
594            }
595            Self::PromptBeforeBlueprint => {
596                Some("Move the `blueprint` block before all `prompt` blocks.")
597            }
598            Self::UnknownTopLevelItem => {
599                Some("Only `blueprint` and `prompt` blocks are valid at the top level.")
600            }
601            Self::UnknownBlueprintAttribute => Some(
602                "Use one of `version`, `name`, `description`, `author`, or `min_achitek_version`.",
603            ),
604            Self::UnknownPromptAttribute => Some(
605                "Use one of `type`, `help`, `choices`, `default`, `required`, `depends_on`, or `validate`.",
606            ),
607            Self::UnknownValidateAttribute => Some(
608                "Use one of `regex`, `min_length`, `max_length`, `min_selections`, or `max_selections`.",
609            ),
610            Self::UnknownPromptType => {
611                Some("Use one of `string`, `paragraph`, `bool`, `select`, or `multiselect`.")
612            }
613            Self::InvalidBooleanLiteral => Some("Use `true` or `false`."),
614            Self::UnterminatedString => Some("Close the string with `\"`."),
615            Self::InvalidEscapeSequence => {
616                Some("Supported escapes are `\\n`, `\\t`, `\\r`, `\\\"`, and `\\\\`.")
617            }
618            Self::InvalidDependencyExpression => Some(
619                "Use a prompt reference, comparison, `contains(...)`, `all(...)`, or `any(...)`.",
620            ),
621            Self::UnknownDependencyMethod => Some("The only supported method is `contains`."),
622            Self::InvalidIdentifier => Some(
623                "Identifiers must start with a letter and contain only letters, digits, or `_`.",
624            ),
625            Self::InvalidInteger => Some("Use a non-negative integer such as `1` or `42`."),
626            Self::MalformedArray => Some("Use `[value, value]` with comma-separated values."),
627            Self::MissingPromptName => Some("Write the prompt name as `prompt \"name\" { ... }`."),
628            Self::MissingAttributeValue => Some("Add a value after `=`, or remove the attribute."),
629            Self::MissingBlueprintVersion => {
630                Some("Add a `version = \"...\"` attribute to the `blueprint` block.")
631            }
632            Self::MissingBlueprintName => {
633                Some("Add a `name = \"...\"` attribute to the `blueprint` block.")
634            }
635            Self::EmptyBlueprintName => Some("Use a non-empty blueprint `name` value."),
636            Self::EmptyBlueprintVersion => Some("Use a non-empty blueprint `version` value."),
637            Self::DuplicateBlueprintAttribute => {
638                Some("Keep one value for each `blueprint` attribute.")
639            }
640            Self::MissingPromptType => Some("Add a `type = ...` attribute to the prompt block."),
641            Self::EmptyPromptName => Some("Use a non-empty prompt name."),
642            Self::DuplicatePromptName => Some("Give each prompt a unique name."),
643            Self::DuplicatePromptAttribute => {
644                Some("Keep one value for each prompt attribute in a prompt block.")
645            }
646            Self::DuplicateValidateAttribute => {
647                Some("Keep one value for each validation attribute in a `validate` block.")
648            }
649            Self::ChoicesOnNonChoicePrompt => {
650                Some("Use `choices` only with `select` or `multiselect` prompts.")
651            }
652            Self::MissingChoicesForSelect => {
653                Some("Add a non-empty `choices = [...]` array to the `select` prompt.")
654            }
655            Self::MissingChoicesForMultiselect => {
656                Some("Add a non-empty `choices = [...]` array to the `multiselect` prompt.")
657            }
658            Self::EmptyChoicesList => Some("Add at least one string choice."),
659            Self::DuplicateChoice => Some("Remove the duplicate choice value."),
660            Self::NonStringChoice => Some("Use string literals for prompt choices."),
661            Self::DefaultTypeMismatch => Some("Use a default value that matches the prompt type."),
662            Self::SelectDefaultNotInChoices => {
663                Some("Set the default to one of the values in `choices`.")
664            }
665            Self::MultiselectDefaultMustBeArray => {
666                Some("Use an array default such as `default = [\"one\"]`.")
667            }
668            Self::MultiselectDefaultContainsUnknownChoice => {
669                Some("Every default value must also appear in `choices`.")
670            }
671            Self::RequiredFalseWithNoDefault => {
672                Some("Remove `required = false` or provide a useful `default` value.")
673            }
674            Self::DuplicateValidateBlock => {
675                Some("Merge validation rules into a single `validate { ... }` block.")
676            }
677            Self::InvalidBlueprintVersion => {
678                Some("Use three numeric version components such as `1.0.0`.")
679            }
680            Self::InvalidMinimumAchitekVersion => {
681                Some("Use three numeric minimum Achitek version components such as `1.0.0`.")
682            }
683            Self::UnknownDependencyReference => {
684                Some("Reference the name of another prompt declared in this file.")
685            }
686            Self::SelfDependency => Some("A prompt cannot depend on itself."),
687            Self::DependencyCycle => Some("Remove or rewrite one dependency to break the cycle."),
688            Self::DependencyTypeMismatch => {
689                Some("Compare dependency values with values that match the referenced prompt type.")
690            }
691            Self::ContainsOnNonMultiselectPrompt => {
692                Some("Use `.contains(...)` only with `multiselect` prompt dependencies.")
693            }
694            Self::ContainsUnknownChoice => {
695                Some("Use a `.contains(...)` value that appears in the referenced prompt choices.")
696            }
697            Self::StringValidationOnNonStringPrompt => Some(
698                "Use string length or regex validation only on `string` or `paragraph` prompts.",
699            ),
700            Self::SelectionValidationOnNonMultiselectPrompt => {
701                Some("Use selection-count validation only on `multiselect` prompts.")
702            }
703            Self::InvalidLengthBounds => {
704                Some("Ensure `min_length` is less than or equal to `max_length`.")
705            }
706            Self::InvalidSelectionBounds => {
707                Some("Ensure `min_selections` is less than or equal to `max_selections`.")
708            }
709            Self::InvalidRegex => Some("Use a regex pattern that can be compiled by Achitek."),
710        }
711    }
712}
713
714/// Severity level for an Achitekfile diagnostic.
715///
716/// Severity indicates how tools should present a diagnostic. Errors describe
717/// invalid source that should prevent normal execution. Warnings describe
718/// suspicious but still analyzable source. Hints provide low-priority guidance.
719///
720/// See [`Diagnostic`] for an example of reading diagnostic severity.
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
722#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
723pub enum Severity {
724    /// Invalid source that should prevent normal execution.
725    Error,
726    /// Suspicious source that can still be analyzed.
727    Warning,
728    /// Low-priority guidance.
729    Hint,
730}
731
732/// A zero-based byte position in Achitekfile source text.
733///
734/// `line` and `byte` use Tree-sitter's native coordinate system: the line is
735/// zero-based and `byte` is the zero-based UTF-8 byte offset from the beginning
736/// of that line.
737///
738/// This type is independent of [LSP] positions. Language-server consumers
739/// should convert `byte` into the negotiated LSP position encoding before
740/// publishing diagnostics or other ranges to an editor.
741///
742/// [LSP]: https://microsoft.github.io/language-server-protocol/
743///
744/// # Examples
745///
746/// ```
747/// let position = achitekfile::TextPosition { line: 3, byte: 12 };
748///
749/// assert_eq!(position.line, 3);
750/// assert_eq!(position.byte, 12);
751/// ```
752#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
753#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
754pub struct TextPosition {
755    /// Zero-based line number.
756    pub line: usize,
757
758    /// Zero-based UTF-8 byte offset within the line.
759    pub byte: usize,
760}
761
762/// A byte range in Achitekfile source text.
763///
764/// The range starts at `start` and ends at `end`, both expressed as zero-based
765/// line plus UTF-8 byte offset positions. Consumers can use this to highlight
766/// diagnostics, symbols, prompt names, attributes, and other source elements
767/// after converting into their presentation protocol's expected position
768/// encoding.
769///
770/// # Examples
771///
772/// ```
773/// let source = r#"
774/// prompt "project_name" {
775///   type = string
776/// }
777/// "#;
778///
779/// let analysis = achitekfile::analyze(source)?;
780/// let range = analysis.diagnostics()[0].range();
781///
782/// assert!(range.start <= range.end);
783/// # Ok::<(), Box<dyn std::error::Error>>(())
784/// ```
785#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
786#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
787pub struct TextRange {
788    /// Start position of the range.
789    pub start: TextPosition,
790
791    /// End position of the range.
792    pub end: TextPosition,
793}