Skip to main content

harn_parser/
diagnostic_codes.rs

1//! Stable diagnostic code registry.
2//!
3//! Codes use `HARN-<CATEGORY>-<NNN>` identifiers so CLI output, editor
4//! diagnostics, docs, and future `harn explain` lookups can refer to one
5//! durable namespace.
6//!
7//! ```
8//! use harn_parser::diagnostic_codes::Category;
9//!
10//! let categories: Vec<_> = Category::ALL.iter().map(|category| category.as_str()).collect();
11//! assert_eq!(
12//!     categories,
13//!     [
14//!         "TYP", "PAR", "NAM", "CAP", "LLM", "ORC", "STD", "PRM",
15//!         "MOD", "LNT", "FMT", "IMP", "OWN", "RCV", "MAT",
16//!     ],
17//! );
18//! ```
19
20use std::fmt;
21use std::str::FromStr;
22
23/// Top-level diagnostic category used in a stable Harn diagnostic code.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub enum Category {
26    Typ,
27    Par,
28    Nam,
29    Cap,
30    Llm,
31    Orc,
32    Std,
33    Prm,
34    Mod,
35    Lnt,
36    Fmt,
37    Imp,
38    Own,
39    Rcv,
40    Mat,
41}
42
43impl Category {
44    pub const ALL: &'static [Category] = &[
45        Category::Typ,
46        Category::Par,
47        Category::Nam,
48        Category::Cap,
49        Category::Llm,
50        Category::Orc,
51        Category::Std,
52        Category::Prm,
53        Category::Mod,
54        Category::Lnt,
55        Category::Fmt,
56        Category::Imp,
57        Category::Own,
58        Category::Rcv,
59        Category::Mat,
60    ];
61
62    pub const fn as_str(self) -> &'static str {
63        match self {
64            Category::Typ => "TYP",
65            Category::Par => "PAR",
66            Category::Nam => "NAM",
67            Category::Cap => "CAP",
68            Category::Llm => "LLM",
69            Category::Orc => "ORC",
70            Category::Std => "STD",
71            Category::Prm => "PRM",
72            Category::Mod => "MOD",
73            Category::Lnt => "LNT",
74            Category::Fmt => "FMT",
75            Category::Imp => "IMP",
76            Category::Own => "OWN",
77            Category::Rcv => "RCV",
78            Category::Mat => "MAT",
79        }
80    }
81}
82
83impl fmt::Display for Category {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(self.as_str())
86    }
87}
88
89/// One registered diagnostic code.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct RegistryEntry {
92    pub code: Code,
93    pub identifier: &'static str,
94    pub category: Category,
95    pub summary: &'static str,
96}
97
98macro_rules! diagnostic_codes {
99    ($($variant:ident, $identifier:literal, $category:ident, $summary:literal;)*) => {
100        /// Stable diagnostic identifier.
101        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
102        pub enum Code {
103            $($variant,)*
104        }
105
106        impl Code {
107            pub const ALL: &'static [Code] = &[
108                $(Code::$variant,)*
109            ];
110
111            pub const fn as_str(self) -> &'static str {
112                match self {
113                    $(Code::$variant => $identifier,)*
114                }
115            }
116
117            pub const fn category(self) -> Category {
118                match self {
119                    $(Code::$variant => Category::$category,)*
120                }
121            }
122
123            pub const fn summary(self) -> &'static str {
124                match self {
125                    $(Code::$variant => $summary,)*
126                }
127            }
128
129            /// Full markdown explanation embedded at compile time. Every
130            /// registered code must ship a matching file under
131            /// `diagnostic_codes/explanations/`; missing files fail the build.
132            pub const fn explanation(self) -> &'static str {
133                match self {
134                    $(Code::$variant => include_str!(
135                        concat!("diagnostic_codes/explanations/", $identifier, ".md")
136                    ),)*
137                }
138            }
139        }
140
141        pub const REGISTRY: &[RegistryEntry] = &[
142            $(RegistryEntry {
143                code: Code::$variant,
144                identifier: $identifier,
145                category: Category::$category,
146                summary: $summary,
147            },)*
148        ];
149    };
150}
151
152diagnostic_codes! {
153    TypeMismatch, "HARN-TYP-001", Typ, "expected and actual types are incompatible";
154    InvalidBinaryOperator, "HARN-TYP-002", Typ, "binary operator is not defined for the operand types";
155    StringInterpolationRewrite, "HARN-TYP-003", Typ, "string concatenation should be rewritten as interpolation";
156    ReturnTypeMismatch, "HARN-TYP-004", Typ, "returned expression does not match the declared return type";
157    AssignmentTypeMismatch, "HARN-TYP-005", Typ, "assigned value does not match the target type";
158    ArgumentTypeMismatch, "HARN-TYP-006", Typ, "argument value does not match the parameter type";
159    VariableTypeMismatch, "HARN-TYP-007", Typ, "initializer does not match the declared variable type";
160    ClosureReturnTypeMismatch, "HARN-TYP-008", Typ, "closure return expression does not match its declared type";
161    FieldTypeMismatch, "HARN-TYP-009", Typ, "field value does not match its declared type";
162    MethodTypeMismatch, "HARN-TYP-010", Typ, "method receiver or result type is incompatible";
163    GenericTypeArgumentUnsupported, "HARN-TYP-011", Typ, "callable does not accept type arguments";
164    GenericTypeArgumentMismatch, "HARN-TYP-012", Typ, "type argument does not satisfy the generic parameter";
165    GenericTypeArgumentArity, "HARN-TYP-013", Typ, "generic call has the wrong number of type arguments";
166    TypeParameterArity, "HARN-TYP-014", Typ, "declaration has the wrong number of type parameters";
167    WhereConstraintMismatch, "HARN-TYP-015", Typ, "type argument does not satisfy a where-clause constraint";
168    IterableExpected, "HARN-TYP-016", Typ, "expression must be iterable";
169    InvalidIndexType, "HARN-TYP-017", Typ, "subscript index type is invalid";
170    CallableExpected, "HARN-TYP-018", Typ, "expression must be callable";
171    InvalidCast, "HARN-TYP-019", Typ, "cast cannot be proven valid";
172    UnknownTypeName, "HARN-TYP-020", Typ, "type name cannot be resolved";
173    InvalidVariantUse, "HARN-TYP-021", Typ, "variant type is used in an invalid position";
174    InvalidStructLiteral, "HARN-TYP-022", Typ, "struct literal is invalid";
175    InvalidEnumConstruct, "HARN-TYP-023", Typ, "enum construction is invalid";
176    InvalidPatternBinding, "HARN-TYP-024", Typ, "pattern binding is invalid for the expected type";
177    InvalidOptionalAccess, "HARN-TYP-025", Typ, "optional access is invalid for the receiver type";
178    ParserUnexpectedToken, "HARN-PAR-001", Par, "parser found an unexpected token";
179    ParserUnexpectedEof, "HARN-PAR-002", Par, "parser reached end of file while expecting syntax";
180    ParserUnexpectedCharacter, "HARN-PAR-003", Par, "lexer found an unexpected character";
181    ParserUnterminatedString, "HARN-PAR-004", Par, "string literal is unterminated";
182    ParserUnterminatedBlockComment, "HARN-PAR-005", Par, "block comment is unterminated";
183    UndefinedVariable, "HARN-NAM-001", Nam, "variable name cannot be resolved";
184    UndefinedFunction, "HARN-NAM-002", Nam, "function name cannot be resolved";
185    UnknownAttribute, "HARN-NAM-003", Nam, "attribute name is not recognized";
186    UnknownField, "HARN-NAM-004", Nam, "field name does not exist on the target type";
187    UnknownMethod, "HARN-NAM-005", Nam, "method name does not exist on the receiver type";
188    DuplicateArgument, "HARN-NAM-006", Nam, "argument name is duplicated";
189    UnknownOption, "HARN-NAM-007", Nam, "option key is not recognized";
190    UnknownBuiltin, "HARN-NAM-008", Nam, "builtin name cannot be resolved";
191    DeprecatedFunction, "HARN-NAM-009", Nam, "function call targets a deprecated declaration";
192    UnknownDeclaration, "HARN-NAM-010", Nam, "declaration reference cannot be resolved";
193    InvalidAttributeTarget, "HARN-NAM-011", Nam, "attribute is attached to an unsupported declaration";
194    InvalidAttributeArgument, "HARN-NAM-012", Nam, "attribute argument is invalid";
195    InvalidMainSignature, "HARN-NAM-101", Nam, "`main` entrypoint must take a single `harness: Harness` parameter";
196    CapabilityPayloadInvalid, "HARN-CAP-001", Cap, "capability payload is invalid";
197    HitlMissingApprovalPolicy, "HARN-CAP-002", Cap, "human approval construct is missing policy";
198    HitlInvalidApprovalArgument, "HARN-CAP-003", Cap, "human approval argument is invalid";
199    CapabilityResultUnchecked, "HARN-CAP-004", Cap, "capability result must be checked";
200    CapabilityUnknownOperation, "HARN-CAP-005", Cap, "host capability operation is not declared";
201    CapabilityCallStaticNameRequired, "HARN-CAP-006", Cap, "host capability call must use a static operation name";
202    CapabilityBindingInvalid, "HARN-CAP-007", Cap, "tool host capability binding is invalid";
203    UnknownLlmOption, "HARN-LLM-001", Llm, "LLM option key is not recognized";
204    DeprecatedLlmOption, "HARN-LLM-002", Llm, "LLM option key is deprecated";
205    LlmSchemaMissing, "HARN-LLM-003", Llm, "LLM call is missing schema validation";
206    LlmSchemaInvalid, "HARN-LLM-004", Llm, "LLM schema option is invalid";
207    LlmProviderIdentityBranch, "HARN-LLM-005", Llm, "prompt branches on provider identity instead of capability flags";
208    OrchestrationArity, "HARN-ORC-001", Orc, "orchestration construct has invalid arity";
209    OrchestrationType, "HARN-ORC-002", Orc, "orchestration construct argument has invalid type";
210    AgentDefinitionInvalid, "HARN-ORC-003", Orc, "agent declaration is invalid";
211    WorkflowDefinitionInvalid, "HARN-ORC-004", Orc, "workflow declaration is invalid";
212    ToolDefinitionInvalid, "HARN-ORC-005", Orc, "tool declaration is invalid";
213    PipelineDefinitionInvalid, "HARN-ORC-006", Orc, "pipeline declaration is invalid";
214    InvalidSelectConstruct, "HARN-ORC-007", Orc, "select construct is invalid";
215    UnreachableCode, "HARN-ORC-008", Orc, "statement cannot be reached";
216    FlowInvariantAttributeInvalid, "HARN-ORC-009", Orc, "Flow invariant attribute set is invalid";
217    ExecutionTargetMissing, "HARN-ORC-010", Orc, "execution target path cannot be found";
218    DeprecatedStdlibSymbol, "HARN-STD-001", Std, "stdlib symbol has been renamed or deprecated";
219    StdlibUsageInvalid, "HARN-STD-002", Std, "stdlib call is invalid";
220    BuiltinArity, "HARN-STD-003", Std, "builtin call has invalid arity";
221    PromptTemplateParse, "HARN-PRM-001", Prm, "prompt template cannot be parsed";
222    PromptVariantExplosion, "HARN-PRM-002", Prm, "prompt template has too many capability-aware branches";
223    PromptInjectionRisk, "HARN-PRM-003", Prm, "prompt construction risks direct injection";
224    PromptProviderIdentityBranch, "HARN-PRM-004", Prm, "prompt template branches on provider identity";
225    PromptToolSurfaceUnknown, "HARN-PRM-005", Prm, "prompt references a tool outside the declared surface";
226    PromptToolSurfaceDeferredReference, "HARN-PRM-006", Prm, "prompt references a deferred tool without tool search";
227    PromptTargetMissing, "HARN-PRM-007", Prm, "prompt or template target cannot be found";
228    ModuleImportUnresolved, "HARN-MOD-001", Mod, "module import cannot be resolved";
229    ModuleImportUnused, "HARN-MOD-002", Mod, "module import is unused";
230    ModuleImportOrder, "HARN-MOD-003", Mod, "module imports are not in canonical order";
231    ModuleExportInvalid, "HARN-MOD-004", Mod, "module export is invalid";
232    ModuleImportCollision, "HARN-MOD-005", Mod, "module imports expose colliding names";
233    ModuleReExportConflict, "HARN-MOD-006", Mod, "module re-exports conflict";
234    LintRenamedStdlibSymbol, "HARN-LNT-001", Lnt, "renamed stdlib symbol lint";
235    LintCyclomaticComplexity, "HARN-LNT-002", Lnt, "cyclomatic complexity lint";
236    LintNamingConvention, "HARN-LNT-003", Lnt, "naming convention lint";
237    LintEagerCollectionConversion, "HARN-LNT-004", Lnt, "eager collection conversion lint";
238    LintRedundantClone, "HARN-LNT-005", Lnt, "redundant clone lint";
239    LintLongRunningWithoutCleanup, "HARN-LNT-006", Lnt, "long-running workflow cleanup lint";
240    LintMcpToolAnnotations, "HARN-LNT-007", Lnt, "MCP tool annotations lint";
241    LintPrOpenWithoutSecretScan, "HARN-LNT-008", Lnt, "PR open without secret scan lint";
242    LintShadowVariable, "HARN-LNT-009", Lnt, "shadow variable lint";
243    LintPersonaHookTarget, "HARN-LNT-010", Lnt, "persona hook target lint";
244    LintDeadCodeAfterReturn, "HARN-LNT-011", Lnt, "dead code after return lint";
245    LintLetThenReturn, "HARN-LNT-012", Lnt, "let then return lint";
246    LintUnhandledApprovalResult, "HARN-LNT-013", Lnt, "unhandled approval result lint";
247    LintUnusedVariable, "HARN-LNT-014", Lnt, "unused variable lint";
248    LintUnusedPatternBinding, "HARN-LNT-015", Lnt, "unused pattern binding lint";
249    LintUnusedParameter, "HARN-LNT-016", Lnt, "unused parameter lint";
250    LintUnusedImport, "HARN-LNT-017", Lnt, "unused import lint";
251    LintMutableNeverReassigned, "HARN-LNT-018", Lnt, "mutable never reassigned lint";
252    LintUnusedFunction, "HARN-LNT-019", Lnt, "unused function lint";
253    LintUnusedType, "HARN-LNT-020", Lnt, "unused type lint";
254    LintPersonaBodyMustCallSteps, "HARN-LNT-021", Lnt, "persona body must call steps lint";
255    LintUndefinedFunction, "HARN-LNT-022", Lnt, "undefined function lint";
256    LintPipelineReturnType, "HARN-LNT-023", Lnt, "pipeline return type lint";
257    LintMissingHarndoc, "HARN-LNT-024", Lnt, "missing harndoc lint";
258    LintAssertOutsideTest, "HARN-LNT-025", Lnt, "assert outside test lint";
259    LintPromptInjectionRisk, "HARN-LNT-026", Lnt, "prompt injection risk lint";
260    LintConnectorEffectPolicy, "HARN-LNT-027", Lnt, "connector effect policy lint";
261    LintUnnecessaryCast, "HARN-LNT-028", Lnt, "unnecessary cast lint";
262    LintUntypedDictAccess, "HARN-LNT-029", Lnt, "untyped dict access lint";
263    LintConstantLogicalOperand, "HARN-LNT-030", Lnt, "constant logical operand lint";
264    LintPointlessComparison, "HARN-LNT-031", Lnt, "pointless comparison lint";
265    LintComparisonToBool, "HARN-LNT-032", Lnt, "comparison to bool lint";
266    LintInvalidBinaryOpLiteral, "HARN-LNT-033", Lnt, "invalid binary operator literal lint";
267    LintRedundantNilTernary, "HARN-LNT-034", Lnt, "redundant nil ternary lint";
268    LintEmptyBlock, "HARN-LNT-035", Lnt, "empty block lint";
269    LintUnnecessaryElseReturn, "HARN-LNT-036", Lnt, "unnecessary else return lint";
270    LintDuplicateMatchArm, "HARN-LNT-037", Lnt, "duplicate match arm lint";
271    LintRequireInTest, "HARN-LNT-038", Lnt, "require in test lint";
272    LintBreakOutsideLoop, "HARN-LNT-039", Lnt, "break outside loop lint";
273    LintTemplateParse, "HARN-LNT-040", Lnt, "template parse lint";
274    LintBlankLineBetweenItems, "HARN-LNT-041", Lnt, "blank line between items lint";
275    LintTrailingComma, "HARN-LNT-042", Lnt, "trailing comma lint";
276    LintUnnecessaryParentheses, "HARN-LNT-043", Lnt, "unnecessary parentheses lint";
277    LintTemplateVariantExplosion, "HARN-LNT-044", Lnt, "template variant explosion lint";
278    LintRequireFileHeader, "HARN-LNT-045", Lnt, "require file header lint";
279    LintTemplateProviderIdentityBranch, "HARN-LNT-046", Lnt, "template provider identity branch lint";
280    LintImportOrder, "HARN-LNT-047", Lnt, "import order lint";
281    LintPreferOptionalShorthand, "HARN-LNT-048", Lnt, "prefer optional shorthand lint";
282    LintLegacyDocComment, "HARN-LNT-049", Lnt, "legacy doc comment lint";
283    LintDeprecatedLlmOptions, "HARN-LNT-050", Lnt, "deprecated LLM options lint";
284    LintUnnecessarySafeNavigation, "HARN-LNT-051", Lnt, "unnecessary safe navigation lint";
285    FormatterParseFailed, "HARN-FMT-001", Fmt, "formatter could not parse the source";
286    FormatterWouldReformat, "HARN-FMT-002", Fmt, "source is not in canonical format";
287    FormatterTrailingComma, "HARN-FMT-003", Fmt, "formatter normalized trailing comma layout";
288    ImportResolutionFailed, "HARN-IMP-001", Imp, "import target cannot be resolved";
289    ImportSymbolMissing, "HARN-IMP-002", Imp, "imported symbol does not exist";
290    ImportCycle, "HARN-IMP-003", Imp, "import graph contains a cycle";
291    ImmutableAssignment, "HARN-OWN-001", Own, "immutable binding is reassigned";
292    MutableNeverReassigned, "HARN-OWN-002", Own, "mutable binding is never reassigned";
293    OwnershipEscape, "HARN-OWN-003", Own, "owned value escapes its valid scope";
294    BoundaryValueUnvalidated, "HARN-OWN-004", Own, "unvalidated boundary value is used directly";
295    RescueOutsideFunction, "HARN-RCV-001", Rcv, "rescue construct is outside a function body";
296    TryOutsideFunction, "HARN-RCV-002", Rcv, "try construct is outside a function body";
297    InvalidRescueConstruct, "HARN-RCV-003", Rcv, "rescue construct is invalid";
298    NonExhaustiveMatch, "HARN-MAT-001", Mat, "match expression is not exhaustive";
299    DuplicateMatchArm, "HARN-MAT-002", Mat, "match expression contains a duplicate arm";
300    InvalidMatchPattern, "HARN-MAT-003", Mat, "match pattern is invalid";
301}
302
303impl Code {
304    pub const fn registry() -> &'static [RegistryEntry] {
305        REGISTRY
306    }
307
308    /// Codes that an agent should consider alongside this one when planning
309    /// repairs. Curated per-code — typically near-neighbours in the same
310    /// category that share a fix shape. Returns an empty slice for codes
311    /// without curated cross-references.
312    pub const fn related(self) -> &'static [Code] {
313        match self {
314            // Type mismatches form a family — surfacing the others helps an
315            // agent disambiguate between assignment, argument, return, etc.
316            Code::TypeMismatch => &[
317                Code::AssignmentTypeMismatch,
318                Code::ArgumentTypeMismatch,
319                Code::ReturnTypeMismatch,
320                Code::VariableTypeMismatch,
321                Code::FieldTypeMismatch,
322            ],
323            Code::AssignmentTypeMismatch => &[Code::TypeMismatch, Code::VariableTypeMismatch],
324            Code::ArgumentTypeMismatch => &[Code::TypeMismatch, Code::GenericTypeArgumentMismatch],
325            Code::ReturnTypeMismatch => &[Code::TypeMismatch, Code::ClosureReturnTypeMismatch],
326            Code::VariableTypeMismatch => &[Code::TypeMismatch, Code::AssignmentTypeMismatch],
327            Code::ClosureReturnTypeMismatch => &[Code::ReturnTypeMismatch],
328            Code::FieldTypeMismatch => &[Code::TypeMismatch, Code::InvalidStructLiteral],
329            Code::MethodTypeMismatch => &[Code::TypeMismatch, Code::CallableExpected],
330            // Generic type-argument family.
331            Code::GenericTypeArgumentUnsupported => &[
332                Code::GenericTypeArgumentMismatch,
333                Code::GenericTypeArgumentArity,
334            ],
335            Code::GenericTypeArgumentMismatch => &[
336                Code::GenericTypeArgumentArity,
337                Code::WhereConstraintMismatch,
338            ],
339            Code::GenericTypeArgumentArity => {
340                &[Code::GenericTypeArgumentMismatch, Code::TypeParameterArity]
341            }
342            Code::TypeParameterArity => &[Code::GenericTypeArgumentArity],
343            Code::WhereConstraintMismatch => &[Code::GenericTypeArgumentMismatch],
344            // Naming.
345            Code::UndefinedVariable => &[Code::UndefinedFunction, Code::UnknownDeclaration],
346            Code::UndefinedFunction => &[Code::UnknownBuiltin, Code::UnknownDeclaration],
347            Code::UnknownField => &[Code::UnknownMethod, Code::InvalidStructLiteral],
348            Code::UnknownMethod => &[Code::UnknownField, Code::CallableExpected],
349            Code::UnknownAttribute => {
350                &[Code::InvalidAttributeArgument, Code::InvalidAttributeTarget]
351            }
352            Code::InvalidAttributeArgument => {
353                &[Code::UnknownAttribute, Code::InvalidAttributeTarget]
354            }
355            Code::InvalidAttributeTarget => {
356                &[Code::UnknownAttribute, Code::InvalidAttributeArgument]
357            }
358            // LLM call family — schema, options, provider branching.
359            Code::LlmSchemaMissing => &[Code::LlmSchemaInvalid, Code::UnknownLlmOption],
360            Code::LlmSchemaInvalid => &[Code::LlmSchemaMissing, Code::UnknownLlmOption],
361            Code::UnknownLlmOption => &[Code::DeprecatedLlmOption, Code::LlmSchemaInvalid],
362            Code::DeprecatedLlmOption => &[Code::UnknownLlmOption],
363            Code::LlmProviderIdentityBranch => &[Code::PromptProviderIdentityBranch],
364            // Prompt-template family.
365            Code::PromptTemplateParse => &[Code::PromptTargetMissing],
366            Code::PromptInjectionRisk => &[Code::LintPromptInjectionRisk],
367            Code::PromptProviderIdentityBranch => &[
368                Code::LlmProviderIdentityBranch,
369                Code::LintTemplateProviderIdentityBranch,
370            ],
371            Code::PromptVariantExplosion => &[Code::LintTemplateVariantExplosion],
372            // Capabilities.
373            Code::CapabilityResultUnchecked => {
374                &[Code::RescueOutsideFunction, Code::TryOutsideFunction]
375            }
376            Code::CapabilityUnknownOperation => &[Code::CapabilityCallStaticNameRequired],
377            // Recovery / match.
378            Code::RescueOutsideFunction => {
379                &[Code::TryOutsideFunction, Code::InvalidRescueConstruct]
380            }
381            Code::TryOutsideFunction => &[Code::RescueOutsideFunction],
382            Code::NonExhaustiveMatch => &[Code::InvalidMatchPattern, Code::DuplicateMatchArm],
383            Code::DuplicateMatchArm => &[Code::NonExhaustiveMatch, Code::LintDuplicateMatchArm],
384            // Module / import family.
385            Code::ModuleImportUnresolved => {
386                &[Code::ImportResolutionFailed, Code::ImportSymbolMissing]
387            }
388            Code::ModuleImportUnused => &[Code::LintUnusedImport],
389            Code::ImportResolutionFailed => {
390                &[Code::ModuleImportUnresolved, Code::ImportSymbolMissing]
391            }
392            Code::ImportCycle => &[Code::ImportResolutionFailed],
393            // Ownership.
394            Code::ImmutableAssignment => &[Code::MutableNeverReassigned],
395            Code::MutableNeverReassigned => &[Code::LintMutableNeverReassigned],
396            // Lint pairs (drift between lint and runtime/typecheck codes).
397            Code::LintDeprecatedLlmOptions => &[Code::DeprecatedLlmOption, Code::UnknownLlmOption],
398            Code::LintPromptInjectionRisk => &[Code::PromptInjectionRisk],
399            Code::LintTemplateVariantExplosion => &[Code::PromptVariantExplosion],
400            Code::LintTemplateProviderIdentityBranch => &[Code::PromptProviderIdentityBranch],
401            Code::LintRenamedStdlibSymbol => &[Code::DeprecatedStdlibSymbol],
402            Code::LintMutableNeverReassigned => &[Code::MutableNeverReassigned],
403            Code::LintUnusedImport => &[Code::ModuleImportUnused],
404            Code::LintDuplicateMatchArm => &[Code::DuplicateMatchArm],
405            _ => &[],
406        }
407    }
408}
409
410impl fmt::Display for Code {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        f.write_str(self.as_str())
413    }
414}
415
416/// Error returned when parsing an unknown diagnostic code.
417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub struct ParseCodeError;
419
420impl fmt::Display for ParseCodeError {
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        f.write_str("unknown Harn diagnostic code")
423    }
424}
425
426impl std::error::Error for ParseCodeError {}
427
428impl FromStr for Code {
429    type Err = ParseCodeError;
430
431    fn from_str(value: &str) -> Result<Self, Self::Err> {
432        Code::ALL
433            .iter()
434            .copied()
435            .find(|code| code.as_str() == value)
436            .ok_or(ParseCodeError)
437    }
438}
439
440/// Autonomy ceiling of a proposed repair.
441///
442/// Agents and IDEs dispatch on this class to decide whether to auto-apply
443/// a fix, propose it as a suggestion, or escalate to a human. Variants
444/// are ordered from least to most disruptive — call sites can compare
445/// with `<=` to enforce a configured ceiling like
446/// `"apply anything up to behavior-preserving"`.
447///
448/// The wire-format strings (`format-only`, `behavior-preserving`, …) are
449/// the contract surface; renaming a variant string is a breaking change.
450#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
451pub enum RepairSafety {
452    /// Whitespace, trivia, or canonical layout only. No code structure
453    /// changes; safe to auto-apply.
454    FormatOnly,
455    /// Intended not to change observable runtime behavior (e.g. delete an
456    /// unreachable branch, drop a redundant cast).
457    BehaviorPreserving,
458    /// Confined to the current local scope or file. Runtime behavior may
459    /// change, but the blast radius does not cross a declaration boundary
460    /// or a public surface.
461    ScopeLocal,
462    /// Touches a signature, export, or call-site surface that other files
463    /// or external consumers can observe.
464    SurfaceChanging,
465    /// Required capabilities or sandbox profile may change as a result of
466    /// applying the repair (e.g. swapping `provider: "openai"` for a
467    /// capability flag widens the routing surface).
468    CapabilityChanging,
469    /// Planning hint only — agents should propose, never auto-apply.
470    /// Aligned with the `AutonomyTier::Suggest`/`ActWithApproval` rungs
471    /// in `trust_graph.rs`.
472    NeedsHuman,
473}
474
475impl RepairSafety {
476    pub const ALL: &'static [RepairSafety] = &[
477        RepairSafety::FormatOnly,
478        RepairSafety::BehaviorPreserving,
479        RepairSafety::ScopeLocal,
480        RepairSafety::SurfaceChanging,
481        RepairSafety::CapabilityChanging,
482        RepairSafety::NeedsHuman,
483    ];
484
485    /// Stable wire-format string. The contract surface — do not rename
486    /// without coordinating with `harn fix --safety <…>` callers and
487    /// downstream LSP/IDE clients.
488    pub const fn as_str(self) -> &'static str {
489        match self {
490            RepairSafety::FormatOnly => "format-only",
491            RepairSafety::BehaviorPreserving => "behavior-preserving",
492            RepairSafety::ScopeLocal => "scope-local",
493            RepairSafety::SurfaceChanging => "surface-changing",
494            RepairSafety::CapabilityChanging => "capability-changing",
495            RepairSafety::NeedsHuman => "needs-human",
496        }
497    }
498
499    /// True when `self` sits at or below `ceiling`. Used by
500    /// `harn fix --apply --safety <ceiling>` and IDE auto-apply policies
501    /// to decide whether a repair clears the configured autonomy bar.
502    pub const fn is_at_most(self, ceiling: RepairSafety) -> bool {
503        (self as u8) <= (ceiling as u8)
504    }
505}
506
507impl fmt::Display for RepairSafety {
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        f.write_str(self.as_str())
510    }
511}
512
513/// Error returned when parsing an unknown repair-safety string.
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub struct ParseRepairSafetyError;
516
517impl fmt::Display for ParseRepairSafetyError {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        f.write_str("unknown Harn repair-safety class")
520    }
521}
522
523impl std::error::Error for ParseRepairSafetyError {}
524
525impl FromStr for RepairSafety {
526    type Err = ParseRepairSafetyError;
527
528    fn from_str(value: &str) -> Result<Self, Self::Err> {
529        RepairSafety::ALL
530            .iter()
531            .copied()
532            .find(|safety| safety.as_str() == value)
533            .ok_or(ParseRepairSafetyError)
534    }
535}
536
537/// Namespaced kebab-case repair identifier (e.g. `imports/fix-path`).
538///
539/// Wraps a `Cow` so registry-driven values reuse a `'static` literal and
540/// per-site overrides can still attach an owned string. The wire-format
541/// string is the contract surface — never normalize or reformat on read.
542#[derive(Debug, Clone, PartialEq, Eq, Hash)]
543pub struct RepairId(std::borrow::Cow<'static, str>);
544
545impl RepairId {
546    pub const fn from_static(s: &'static str) -> Self {
547        RepairId(std::borrow::Cow::Borrowed(s))
548    }
549
550    pub fn from_owned(s: String) -> Self {
551        RepairId(std::borrow::Cow::Owned(s))
552    }
553
554    pub fn as_str(&self) -> &str {
555        &self.0
556    }
557}
558
559impl fmt::Display for RepairId {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        f.write_str(&self.0)
562    }
563}
564
565/// A structured repair proposal attached to a diagnostic.
566///
567/// `id` and `summary` are agent-readable metadata; `safety` is the
568/// dispatch dimension that decides whether the repair clears an
569/// autonomy ceiling. The concrete edits, when known statically, live on
570/// the diagnostic's `fix: Option<Vec<FixEdit>>`; this `Repair` is the
571/// classifier above those edits, not a replacement for them.
572#[derive(Debug, Clone)]
573pub struct Repair {
574    pub id: RepairId,
575    pub summary: String,
576    pub safety: RepairSafety,
577}
578
579impl Repair {
580    pub fn from_template(template: &RepairTemplate) -> Self {
581        Repair {
582            id: RepairId::from_static(template.id),
583            summary: template.summary.to_string(),
584            safety: template.safety,
585        }
586    }
587}
588
589/// Static-lifetime repair template bound to a diagnostic code.
590///
591/// Stored in the registry alongside `Code`. Construction sites can
592/// materialize a `Repair` via [`Repair::from_template`] or override
593/// `summary` for instance-specific detail by building a `Repair`
594/// directly.
595#[derive(Debug, Clone, Copy)]
596pub struct RepairTemplate {
597    pub id: &'static str,
598    pub summary: &'static str,
599    pub safety: RepairSafety,
600}
601
602impl Code {
603    /// Look up the default repair template attached to this diagnostic
604    /// code, or `None` if no actionable fix shape is registered.
605    pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
606        match self {
607            // --- TYP: type mismatches & coercions -------------------------
608            Code::TypeMismatch
609            | Code::ReturnTypeMismatch
610            | Code::AssignmentTypeMismatch
611            | Code::ArgumentTypeMismatch
612            | Code::VariableTypeMismatch
613            | Code::ClosureReturnTypeMismatch
614            | Code::FieldTypeMismatch
615            | Code::MethodTypeMismatch
616            | Code::InvalidIndexType => Some(&REPAIR_INSERT_EXPLICIT_CONVERSION),
617            Code::StringInterpolationRewrite => Some(&REPAIR_REWRITE_STRING_INTERPOLATION),
618            Code::UnknownTypeName => Some(&REPAIR_IMPORTS_FIX_PATH),
619            Code::InvalidCast => Some(&REPAIR_CASTS_REMOVE_UNCHECKED),
620
621            // --- NAM / IMP: imports & names -------------------------------
622            Code::UndefinedVariable
623            | Code::UndefinedFunction
624            | Code::UnknownField
625            | Code::UnknownMethod
626            | Code::UnknownBuiltin
627            | Code::UnknownDeclaration => Some(&REPAIR_BINDINGS_RENAME_TO_CLOSEST),
628            Code::InvalidMainSignature => Some(&REPAIR_BINDINGS_THREAD_HARNESS),
629            Code::DeprecatedFunction => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
630            Code::ModuleImportUnresolved | Code::ImportResolutionFailed => {
631                Some(&REPAIR_IMPORTS_FIX_PATH)
632            }
633            Code::ModuleImportUnused => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
634            Code::ModuleImportOrder => Some(&REPAIR_IMPORTS_REORDER),
635
636            // --- CAP / RCV: capabilities & error recovery -----------------
637            Code::CapabilityResultUnchecked => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
638            Code::CapabilityBindingInvalid => Some(&REPAIR_MANUAL_REVIEW_CAPABILITY),
639            Code::RescueOutsideFunction | Code::TryOutsideFunction => {
640                Some(&REPAIR_ERRORS_WRAP_IN_FN)
641            }
642
643            // --- LLM / PRM: model + prompt contract -----------------------
644            Code::DeprecatedLlmOption => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
645            Code::LlmSchemaMissing => Some(&REPAIR_LLM_ADD_SCHEMA),
646            Code::LlmProviderIdentityBranch | Code::PromptProviderIdentityBranch => {
647                Some(&REPAIR_LLM_USE_CAPABILITY_FLAG)
648            }
649            Code::PromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
650            Code::PromptToolSurfaceUnknown | Code::PromptToolSurfaceDeferredReference => {
651                Some(&REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE)
652            }
653            Code::PromptVariantExplosion => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
654
655            // --- STD: stdlib usage ----------------------------------------
656            Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
657
658            // --- OWN: ownership & mutability ------------------------------
659            Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
660            Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
661
662            // --- MAT: match exhaustiveness --------------------------------
663            Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
664            Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
665
666            // --- ORC: orchestration ---------------------------------------
667            Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
668
669            // --- FMT: formatter -------------------------------------------
670            Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
671                Some(&REPAIR_FORMAT_REFORMAT)
672            }
673
674            // --- LNT: lints with structured fixes -------------------------
675            Code::LintUnusedVariable
676            | Code::LintUnusedPatternBinding
677            | Code::LintUnusedParameter => Some(&REPAIR_BINDINGS_RENAME_UNUSED),
678            Code::LintUnusedImport => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
679            Code::LintUnusedFunction | Code::LintUnusedType => {
680                Some(&REPAIR_DECLARATIONS_REMOVE_UNUSED)
681            }
682            Code::LintMutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
683            Code::LintImportOrder => Some(&REPAIR_IMPORTS_REORDER),
684            Code::LintBlankLineBetweenItems
685            | Code::LintTrailingComma
686            | Code::LintUnnecessaryParentheses
687            | Code::LintRequireFileHeader => Some(&REPAIR_FORMAT_REFORMAT),
688            Code::LintLegacyDocComment => Some(&REPAIR_DOC_COMMENT_MIGRATE),
689            Code::LintEmptyBlock => Some(&REPAIR_BLOCK_REMOVE_EMPTY),
690            Code::LintUnnecessaryElseReturn | Code::LintLetThenReturn => {
691                Some(&REPAIR_CONTROL_FLOW_FLATTEN)
692            }
693            Code::LintRedundantNilTernary
694            | Code::LintUnnecessarySafeNavigation
695            | Code::LintPreferOptionalShorthand
696            | Code::LintComparisonToBool
697            | Code::LintPointlessComparison
698            | Code::LintConstantLogicalOperand => Some(&REPAIR_EXPRESSION_SIMPLIFY),
699            Code::LintUnnecessaryCast => Some(&REPAIR_CASTS_REMOVE_REDUNDANT),
700            Code::LintRedundantClone => Some(&REPAIR_CLONE_REMOVE_REDUNDANT),
701            Code::LintEagerCollectionConversion => Some(&REPAIR_COLLECTION_PREFER_LAZY),
702            Code::LintDeadCodeAfterReturn => Some(&REPAIR_DEAD_CODE_REMOVE),
703            Code::LintRenamedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
704            Code::LintDeprecatedLlmOptions => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
705            Code::LintTemplateProviderIdentityBranch => Some(&REPAIR_LLM_USE_CAPABILITY_FLAG),
706            Code::LintPromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
707            Code::LintShadowVariable => Some(&REPAIR_BINDINGS_RENAME_SHADOW),
708            Code::LintNamingConvention => Some(&REPAIR_STYLE_RENAME_TO_CONVENTION),
709            Code::LintUnhandledApprovalResult => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
710            Code::LintMissingHarndoc => Some(&REPAIR_DOC_ADD_HARNDOC),
711            Code::LintDuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
712            Code::LintUntypedDictAccess => Some(&REPAIR_TYPES_ADD_SHAPE_ANNOTATION),
713            Code::LintMcpToolAnnotations => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
714            Code::LintTemplateVariantExplosion | Code::LintLongRunningWithoutCleanup => {
715                Some(&REPAIR_MANUAL_NEEDS_HUMAN)
716            }
717
718            // Everything else: no statically known repair shape. Agents
719            // should treat these as "diagnose only" until a repair is
720            // registered.
721            _ => None,
722        }
723    }
724}
725
726// Repair-id catalog. Each `RepairTemplate` carries a kebab-case
727// namespaced id (`<namespace>/<verb-noun>`), a one-line summary written
728// in the imperative voice, and a `RepairSafety` class.
729//
730// Conventions:
731//   - Namespaces stay short: `bindings/`, `imports/`, `errors/`, `casts/`,
732//     `format/`, `llm/`, `prompts/`, `match/`, `stdlib/`, `lint/`,
733//     `doc/`, `style/`, `types/`, `manual/`.
734//   - Summary starts with a verb ("Replace…", "Remove…", "Insert…").
735//   - Safety must be the most permissive class that is still always true
736//     for every site this template attaches to. When unsure, pick the
737//     stricter class — agents tighten too-loose policies later, never
738//     too-tight ones.
739
740const REPAIR_INSERT_EXPLICIT_CONVERSION: RepairTemplate = RepairTemplate {
741    id: "casts/insert-explicit-conversion",
742    summary: "Insert an explicit conversion or correct the operand type",
743    safety: RepairSafety::ScopeLocal,
744};
745
746const REPAIR_REWRITE_STRING_INTERPOLATION: RepairTemplate = RepairTemplate {
747    id: "style/string-interpolation",
748    summary: "Rewrite string concatenation as an interpolation literal",
749    safety: RepairSafety::BehaviorPreserving,
750};
751
752const REPAIR_CASTS_REMOVE_UNCHECKED: RepairTemplate = RepairTemplate {
753    id: "casts/remove-unchecked",
754    summary: "Remove the unchecked cast or guard it with a type test",
755    safety: RepairSafety::ScopeLocal,
756};
757
758const REPAIR_CASTS_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
759    id: "casts/remove-redundant",
760    summary: "Remove the redundant cast",
761    safety: RepairSafety::BehaviorPreserving,
762};
763
764const REPAIR_BINDINGS_RENAME_TO_CLOSEST: RepairTemplate = RepairTemplate {
765    id: "bindings/rename-to-closest",
766    summary: "Rename to the closest in-scope identifier",
767    safety: RepairSafety::ScopeLocal,
768};
769
770const REPAIR_BINDINGS_MAKE_MUTABLE: RepairTemplate = RepairTemplate {
771    id: "bindings/make-mutable",
772    summary: "Mark the binding `mut` so it can be reassigned",
773    safety: RepairSafety::ScopeLocal,
774};
775
776const REPAIR_BINDINGS_MAKE_IMMUTABLE: RepairTemplate = RepairTemplate {
777    id: "bindings/make-immutable",
778    summary: "Drop `mut` since the binding is never reassigned",
779    safety: RepairSafety::BehaviorPreserving,
780};
781
782const REPAIR_BINDINGS_RENAME_UNUSED: RepairTemplate = RepairTemplate {
783    id: "bindings/rename-unused",
784    summary: "Prefix the unused binding with `_` to silence the lint",
785    safety: RepairSafety::BehaviorPreserving,
786};
787
788const REPAIR_BINDINGS_RENAME_SHADOW: RepairTemplate = RepairTemplate {
789    id: "bindings/rename-shadow",
790    summary: "Rename the shadowing binding to a distinct name",
791    safety: RepairSafety::ScopeLocal,
792};
793
794const REPAIR_BINDINGS_THREAD_HARNESS: RepairTemplate = RepairTemplate {
795    id: "bindings/thread-harness",
796    summary: "Rewrite the entrypoint as `fn main(harness: Harness)` so the runtime can thread its capability handle",
797    safety: RepairSafety::SurfaceChanging,
798};
799
800const REPAIR_DECLARATIONS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
801    id: "declarations/remove-unused",
802    summary: "Remove the unused declaration",
803    safety: RepairSafety::SurfaceChanging,
804};
805
806const REPAIR_IMPORTS_FIX_PATH: RepairTemplate = RepairTemplate {
807    id: "imports/fix-path",
808    summary: "Replace the import path with a resolvable target",
809    safety: RepairSafety::ScopeLocal,
810};
811
812const REPAIR_IMPORTS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
813    id: "imports/remove-unused",
814    summary: "Remove the unused import",
815    safety: RepairSafety::BehaviorPreserving,
816};
817
818const REPAIR_IMPORTS_REORDER: RepairTemplate = RepairTemplate {
819    id: "imports/reorder",
820    summary: "Reorder imports into canonical grouping",
821    safety: RepairSafety::FormatOnly,
822};
823
824const REPAIR_ERRORS_CHECK_OR_RESCUE: RepairTemplate = RepairTemplate {
825    id: "errors/check-or-rescue",
826    summary: "Check the result or wrap the call in a `rescue` block",
827    safety: RepairSafety::ScopeLocal,
828};
829
830const REPAIR_ERRORS_WRAP_IN_FN: RepairTemplate = RepairTemplate {
831    id: "errors/wrap-in-fn",
832    summary: "Move the construct inside a function body",
833    safety: RepairSafety::SurfaceChanging,
834};
835
836const REPAIR_MATCH_ADD_MISSING_ARMS: RepairTemplate = RepairTemplate {
837    id: "match/add-missing-arms",
838    summary: "Add arms covering the missing variants",
839    safety: RepairSafety::ScopeLocal,
840};
841
842const REPAIR_MATCH_REMOVE_DUPLICATE_ARM: RepairTemplate = RepairTemplate {
843    id: "match/remove-duplicate-arm",
844    summary: "Remove the duplicated match arm",
845    safety: RepairSafety::BehaviorPreserving,
846};
847
848const REPAIR_FORMAT_REFORMAT: RepairTemplate = RepairTemplate {
849    id: "format/reformat",
850    summary: "Apply canonical formatting",
851    safety: RepairSafety::FormatOnly,
852};
853
854const REPAIR_DOC_COMMENT_MIGRATE: RepairTemplate = RepairTemplate {
855    id: "doc/migrate-comment-style",
856    summary: "Migrate the legacy comment to canonical doc syntax",
857    safety: RepairSafety::FormatOnly,
858};
859
860const REPAIR_DOC_ADD_HARNDOC: RepairTemplate = RepairTemplate {
861    id: "doc/add-harndoc",
862    summary: "Add a `///` doc comment describing this declaration",
863    safety: RepairSafety::BehaviorPreserving,
864};
865
866const REPAIR_BLOCK_REMOVE_EMPTY: RepairTemplate = RepairTemplate {
867    id: "blocks/remove-empty",
868    summary: "Remove the empty block or fill in an explicit body",
869    safety: RepairSafety::ScopeLocal,
870};
871
872const REPAIR_CONTROL_FLOW_FLATTEN: RepairTemplate = RepairTemplate {
873    id: "control-flow/flatten",
874    summary: "Flatten the unnecessary control flow construct",
875    safety: RepairSafety::BehaviorPreserving,
876};
877
878const REPAIR_EXPRESSION_SIMPLIFY: RepairTemplate = RepairTemplate {
879    id: "expressions/simplify",
880    summary: "Simplify the expression to its canonical form",
881    safety: RepairSafety::BehaviorPreserving,
882};
883
884const REPAIR_CLONE_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
885    id: "clones/remove-redundant",
886    summary: "Remove the redundant clone",
887    safety: RepairSafety::BehaviorPreserving,
888};
889
890const REPAIR_COLLECTION_PREFER_LAZY: RepairTemplate = RepairTemplate {
891    id: "collections/prefer-lazy",
892    summary: "Replace the eager collection step with a lazy variant",
893    safety: RepairSafety::ScopeLocal,
894};
895
896const REPAIR_DEAD_CODE_REMOVE: RepairTemplate = RepairTemplate {
897    id: "control-flow/remove-dead",
898    summary: "Remove the unreachable code",
899    safety: RepairSafety::BehaviorPreserving,
900};
901
902const REPAIR_STDLIB_MIGRATE_RENAMED: RepairTemplate = RepairTemplate {
903    id: "stdlib/migrate-renamed",
904    summary: "Rename the call to the renamed stdlib symbol",
905    safety: RepairSafety::ScopeLocal,
906};
907
908const REPAIR_LLM_MIGRATE_DEPRECATED_OPTION: RepairTemplate = RepairTemplate {
909    id: "llm/migrate-deprecated-option",
910    summary: "Replace the deprecated option with its supported equivalent",
911    safety: RepairSafety::ScopeLocal,
912};
913
914const REPAIR_LLM_ADD_SCHEMA: RepairTemplate = RepairTemplate {
915    id: "llm/add-schema",
916    summary: "Add a typed output schema to the LLM call",
917    safety: RepairSafety::SurfaceChanging,
918};
919
920const REPAIR_LLM_USE_CAPABILITY_FLAG: RepairTemplate = RepairTemplate {
921    id: "llm/use-capability-flag",
922    summary: "Branch on a capability flag instead of provider identity",
923    safety: RepairSafety::CapabilityChanging,
924};
925
926const REPAIR_PROMPTS_ESCAPE_INJECTION: RepairTemplate = RepairTemplate {
927    id: "prompts/escape-injection",
928    summary: "Pass the untrusted input through a structured placeholder",
929    safety: RepairSafety::ScopeLocal,
930};
931
932const REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE: RepairTemplate = RepairTemplate {
933    id: "prompts/add-tool-to-surface",
934    summary: "Add the referenced tool to the declared tool surface",
935    safety: RepairSafety::SurfaceChanging,
936};
937
938const REPAIR_STYLE_RENAME_TO_CONVENTION: RepairTemplate = RepairTemplate {
939    id: "style/rename-to-convention",
940    summary: "Rename to match the casing convention for this kind",
941    safety: RepairSafety::SurfaceChanging,
942};
943
944const REPAIR_TYPES_ADD_SHAPE_ANNOTATION: RepairTemplate = RepairTemplate {
945    id: "types/add-shape-annotation",
946    summary: "Annotate the dict with a concrete shape type",
947    safety: RepairSafety::SurfaceChanging,
948};
949
950const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
951    id: "manual/review-capability-binding",
952    summary: "Review the capability binding; the fix is not mechanical",
953    safety: RepairSafety::NeedsHuman,
954};
955
956const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
957    id: "manual/needs-human",
958    summary: "Plan a human-led change; auto-apply is not safe here",
959    safety: RepairSafety::NeedsHuman,
960};
961
962/// Every distinct repair template registered by [`Code::repair_template`],
963/// in source order. Used by the catalog wire-up in E1.7 and by tests
964/// asserting the catalog is healthy.
965pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
966    &REPAIR_INSERT_EXPLICIT_CONVERSION,
967    &REPAIR_REWRITE_STRING_INTERPOLATION,
968    &REPAIR_CASTS_REMOVE_UNCHECKED,
969    &REPAIR_CASTS_REMOVE_REDUNDANT,
970    &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
971    &REPAIR_BINDINGS_MAKE_MUTABLE,
972    &REPAIR_BINDINGS_MAKE_IMMUTABLE,
973    &REPAIR_BINDINGS_RENAME_UNUSED,
974    &REPAIR_BINDINGS_RENAME_SHADOW,
975    &REPAIR_BINDINGS_THREAD_HARNESS,
976    &REPAIR_DECLARATIONS_REMOVE_UNUSED,
977    &REPAIR_IMPORTS_FIX_PATH,
978    &REPAIR_IMPORTS_REMOVE_UNUSED,
979    &REPAIR_IMPORTS_REORDER,
980    &REPAIR_ERRORS_CHECK_OR_RESCUE,
981    &REPAIR_ERRORS_WRAP_IN_FN,
982    &REPAIR_MATCH_ADD_MISSING_ARMS,
983    &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
984    &REPAIR_FORMAT_REFORMAT,
985    &REPAIR_DOC_COMMENT_MIGRATE,
986    &REPAIR_DOC_ADD_HARNDOC,
987    &REPAIR_BLOCK_REMOVE_EMPTY,
988    &REPAIR_CONTROL_FLOW_FLATTEN,
989    &REPAIR_EXPRESSION_SIMPLIFY,
990    &REPAIR_CLONE_REMOVE_REDUNDANT,
991    &REPAIR_COLLECTION_PREFER_LAZY,
992    &REPAIR_DEAD_CODE_REMOVE,
993    &REPAIR_STDLIB_MIGRATE_RENAMED,
994    &REPAIR_LLM_MIGRATE_DEPRECATED_OPTION,
995    &REPAIR_LLM_ADD_SCHEMA,
996    &REPAIR_LLM_USE_CAPABILITY_FLAG,
997    &REPAIR_PROMPTS_ESCAPE_INJECTION,
998    &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
999    &REPAIR_STYLE_RENAME_TO_CONVENTION,
1000    &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
1001    &REPAIR_MANUAL_REVIEW_CAPABILITY,
1002    &REPAIR_MANUAL_NEEDS_HUMAN,
1003];
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::{Category, Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
1008    use std::collections::HashSet;
1009    use std::str::FromStr;
1010
1011    #[test]
1012    fn parses_registered_code() {
1013        assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
1014    }
1015
1016    #[test]
1017    fn registry_has_unique_identifiers() {
1018        let mut seen = HashSet::new();
1019        for entry in Code::registry() {
1020            assert!(
1021                seen.insert(entry.identifier),
1022                "duplicate diagnostic code {}",
1023                entry.identifier
1024            );
1025            assert_eq!(entry.code.as_str(), entry.identifier);
1026            assert_eq!(entry.code.category(), entry.category);
1027            let expected_prefix = format!("HARN-{}-", entry.category);
1028            assert!(entry.identifier.starts_with(&expected_prefix));
1029            let suffix = entry.identifier.trim_start_matches(&expected_prefix);
1030            assert_eq!(suffix.len(), 3);
1031            assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
1032            assert!(!entry.summary.is_empty());
1033        }
1034        assert!(Code::registry().len() >= 40);
1035    }
1036
1037    #[test]
1038    fn every_category_is_populated() {
1039        for category in Category::ALL {
1040            assert!(
1041                Code::registry()
1042                    .iter()
1043                    .any(|entry| entry.category == *category),
1044                "missing diagnostic code category {category}"
1045            );
1046        }
1047    }
1048
1049    #[test]
1050    fn every_code_has_non_empty_explanation() {
1051        for entry in Code::registry() {
1052            let body = entry.code.explanation();
1053            assert!(
1054                !body.trim().is_empty(),
1055                "diagnostic code {} has an empty explanation file",
1056                entry.identifier
1057            );
1058            assert!(
1059                body.contains(entry.identifier),
1060                "explanation for {} should reference its identifier",
1061                entry.identifier
1062            );
1063        }
1064    }
1065
1066    #[test]
1067    fn related_codes_are_registered_and_non_self() {
1068        for entry in Code::registry() {
1069            for &other in entry.code.related() {
1070                assert_ne!(
1071                    other, entry.code,
1072                    "{} lists itself as a related code",
1073                    entry.identifier
1074                );
1075                assert!(
1076                    Code::registry().iter().any(|e| e.code == other),
1077                    "{} lists unregistered related code {}",
1078                    entry.identifier,
1079                    other
1080                );
1081            }
1082        }
1083    }
1084
1085    #[test]
1086    fn repair_safety_string_roundtrip() {
1087        for safety in RepairSafety::ALL {
1088            let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
1089            assert_eq!(parsed, *safety);
1090            assert_eq!(parsed.to_string(), safety.as_str());
1091        }
1092        assert_eq!(
1093            RepairSafety::from_str("not-a-safety-class"),
1094            Err(ParseRepairSafetyError)
1095        );
1096    }
1097
1098    #[test]
1099    fn repair_safety_ordering_is_monotonic_low_to_high() {
1100        // The is_at_most ceiling check relies on this ordering being
1101        // least-to-most disruptive; a regression here flips the meaning
1102        // of `harn fix --safety <ceiling>` for every caller.
1103        let order = RepairSafety::ALL;
1104        for window in order.windows(2) {
1105            assert!(
1106                window[0] < window[1],
1107                "{:?} should be safer than {:?}",
1108                window[0],
1109                window[1]
1110            );
1111            assert!(window[0].is_at_most(window[1]));
1112            assert!(!window[1].is_at_most(window[0]));
1113        }
1114    }
1115
1116    #[test]
1117    fn repair_registry_has_at_least_twenty_entries() {
1118        assert!(
1119            REPAIR_REGISTRY.len() >= 20,
1120            "expected ≥20 repair templates, found {}",
1121            REPAIR_REGISTRY.len()
1122        );
1123    }
1124
1125    #[test]
1126    fn repair_ids_are_kebab_case_namespaced_and_unique() {
1127        let mut seen = HashSet::new();
1128        for template in REPAIR_REGISTRY {
1129            assert!(
1130                seen.insert(template.id),
1131                "duplicate repair id {}",
1132                template.id
1133            );
1134            let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
1135                panic!(
1136                    "repair id `{}` is missing `<namespace>/` prefix",
1137                    template.id
1138                )
1139            });
1140            assert!(
1141                !namespace.is_empty() && !leaf.is_empty(),
1142                "repair id `{}` has empty namespace or leaf",
1143                template.id
1144            );
1145            for ch in template.id.chars() {
1146                assert!(
1147                    ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
1148                    "repair id `{}` has non-kebab character {ch:?}",
1149                    template.id
1150                );
1151            }
1152            assert!(
1153                !template.summary.is_empty(),
1154                "repair {} has empty summary",
1155                template.id
1156            );
1157            // Summaries are imperative: start with a capital ASCII letter.
1158            let first = template.summary.chars().next().unwrap();
1159            assert!(
1160                first.is_ascii_uppercase(),
1161                "repair {} summary `{}` should start with a capital",
1162                template.id,
1163                template.summary
1164            );
1165        }
1166    }
1167
1168    #[test]
1169    fn manual_namespace_is_needs_human() {
1170        for template in REPAIR_REGISTRY {
1171            if let Some(("manual", _)) = template.id.split_once('/') {
1172                assert_eq!(
1173                    template.safety,
1174                    RepairSafety::NeedsHuman,
1175                    "manual/* repair {} must be NeedsHuman",
1176                    template.id
1177                );
1178            }
1179        }
1180    }
1181
1182    #[test]
1183    fn known_codes_carry_expected_safety_class() {
1184        // Spot-check: the autonomy contract for several representative
1185        // diagnostics. Lock in the safety class so cross-repo agents that
1186        // dispatch on these don't silently drift when the catalog moves.
1187        let expected: &[(Code, RepairSafety, &str)] = &[
1188            (
1189                Code::FormatterWouldReformat,
1190                RepairSafety::FormatOnly,
1191                "format/reformat",
1192            ),
1193            (
1194                Code::ModuleImportUnused,
1195                RepairSafety::BehaviorPreserving,
1196                "imports/remove-unused",
1197            ),
1198            (
1199                Code::ImmutableAssignment,
1200                RepairSafety::ScopeLocal,
1201                "bindings/make-mutable",
1202            ),
1203            (
1204                Code::LintUnusedFunction,
1205                RepairSafety::SurfaceChanging,
1206                "declarations/remove-unused",
1207            ),
1208            (
1209                Code::LlmProviderIdentityBranch,
1210                RepairSafety::CapabilityChanging,
1211                "llm/use-capability-flag",
1212            ),
1213            (
1214                Code::PromptVariantExplosion,
1215                RepairSafety::NeedsHuman,
1216                "manual/needs-human",
1217            ),
1218            (
1219                Code::NonExhaustiveMatch,
1220                RepairSafety::ScopeLocal,
1221                "match/add-missing-arms",
1222            ),
1223        ];
1224        for (code, safety, repair_id) in expected {
1225            let template = code
1226                .repair_template()
1227                .unwrap_or_else(|| panic!("{code} should have a repair template"));
1228            assert_eq!(template.safety, *safety, "{code} safety class drifted");
1229            assert_eq!(template.id, *repair_id, "{code} repair id drifted");
1230        }
1231    }
1232
1233    #[test]
1234    fn repair_templates_cover_at_least_twenty_codes() {
1235        let covered = Code::ALL
1236            .iter()
1237            .filter(|code| code.repair_template().is_some())
1238            .count();
1239        assert!(
1240            covered >= 20,
1241            "expected ≥20 codes with a repair template, found {covered}"
1242        );
1243    }
1244
1245    #[test]
1246    fn every_registered_repair_is_referenced_by_some_code() {
1247        let referenced: HashSet<&'static str> = Code::ALL
1248            .iter()
1249            .filter_map(|code| code.repair_template())
1250            .map(|template| template.id)
1251            .collect();
1252        for template in REPAIR_REGISTRY {
1253            assert!(
1254                referenced.contains(template.id),
1255                "repair {} is in REPAIR_REGISTRY but no Code maps to it",
1256                template.id
1257            );
1258        }
1259    }
1260}