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