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