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