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