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}