Skip to main content

harn_parser/diagnostic_codes/
repairs.rs

1//! Repair vocabulary attached to a diagnostic code.
2//!
3//! A diagnostic says what is wrong; a repair says what an agent or editor may
4//! do about it and how far that action is allowed to reach. The safety ladder
5//! here is a contract surface — `harn fix --safety <class>` and IDE auto-apply
6//! ceilings both compare against it — so the wire strings are as stable as the
7//! codes themselves.
8//!
9//! Split out of `diagnostic_codes.rs` (#6126), which had reached its
10//! source-length ceiling and could not accept another code. The registry keeps
11//! the codes; this module keeps everything that answers "and what do I do".
12
13use std::{fmt, str::FromStr};
14
15use super::Code;
16
17/// Autonomy ceiling of a proposed repair.
18///
19/// Agents and IDEs use this class to auto-apply, suggest, or escalate a fix. Variants
20/// are ordered from least to most disruptive — call sites can compare
21/// with `<=` to enforce a configured ceiling like
22/// `"apply anything up to behavior-preserving"`.
23///
24/// The wire-format strings (`format-only`, `behavior-preserving`, …) are
25/// the contract surface; renaming a variant string is a breaking change.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum RepairSafety {
28    /// Whitespace, trivia, or canonical layout only. No code structure
29    /// changes; safe to auto-apply.
30    FormatOnly,
31    /// Intended not to change observable runtime behavior (e.g. delete an
32    /// unreachable branch, drop a redundant cast).
33    BehaviorPreserving,
34    /// Confined to the current local scope or file. Runtime behavior may
35    /// change, but the blast radius does not cross a declaration boundary
36    /// or a public surface.
37    ScopeLocal,
38    /// Touches a signature, export, or call-site surface that other files
39    /// or external consumers can observe.
40    SurfaceChanging,
41    /// Required capabilities or sandbox profile may change as a result of
42    /// applying the repair (e.g. swapping `provider: "openai"` for a
43    /// capability flag widens the routing surface).
44    CapabilityChanging,
45    /// Planning hint only — agents should propose, never auto-apply.
46    /// Aligned with the `AutonomyTier::Suggest`/`ActWithApproval` rungs
47    /// in `trust_graph.rs`.
48    NeedsHuman,
49}
50
51impl RepairSafety {
52    pub const ALL: &'static [RepairSafety] = &[
53        RepairSafety::FormatOnly,
54        RepairSafety::BehaviorPreserving,
55        RepairSafety::ScopeLocal,
56        RepairSafety::SurfaceChanging,
57        RepairSafety::CapabilityChanging,
58        RepairSafety::NeedsHuman,
59    ];
60
61    /// Stable wire-format string. The contract surface — do not rename
62    /// without coordinating with `harn fix --safety <…>` callers and
63    /// downstream LSP/IDE clients.
64    pub const fn as_str(self) -> &'static str {
65        match self {
66            RepairSafety::FormatOnly => "format-only",
67            RepairSafety::BehaviorPreserving => "behavior-preserving",
68            RepairSafety::ScopeLocal => "scope-local",
69            RepairSafety::SurfaceChanging => "surface-changing",
70            RepairSafety::CapabilityChanging => "capability-changing",
71            RepairSafety::NeedsHuman => "needs-human",
72        }
73    }
74
75    /// True when `self` sits at or below `ceiling`. Used by
76    /// `harn fix --apply --safety <ceiling>` and IDE auto-apply policies
77    /// to decide whether a repair clears the configured autonomy bar.
78    pub const fn is_at_most(self, ceiling: RepairSafety) -> bool {
79        (self as u8) <= (ceiling as u8)
80    }
81}
82
83impl fmt::Display for RepairSafety {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(self.as_str())
86    }
87}
88
89/// Error returned when parsing an unknown repair-safety string.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct ParseRepairSafetyError;
92
93impl fmt::Display for ParseRepairSafetyError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.write_str("unknown Harn repair-safety class")
96    }
97}
98
99impl std::error::Error for ParseRepairSafetyError {}
100
101impl FromStr for RepairSafety {
102    type Err = ParseRepairSafetyError;
103
104    fn from_str(value: &str) -> Result<Self, Self::Err> {
105        RepairSafety::ALL
106            .iter()
107            .copied()
108            .find(|safety| safety.as_str() == value)
109            .ok_or(ParseRepairSafetyError)
110    }
111}
112
113/// Namespaced kebab-case repair identifier (e.g. `imports/fix-path`).
114///
115/// Wraps a `Cow` so registry-driven values reuse a `'static` literal and
116/// per-site overrides can still attach an owned string. The wire-format
117/// string is the contract surface — never normalize or reformat on read.
118#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub struct RepairId(std::borrow::Cow<'static, str>);
120
121impl RepairId {
122    pub const fn from_static(s: &'static str) -> Self {
123        RepairId(std::borrow::Cow::Borrowed(s))
124    }
125
126    pub fn from_owned(s: String) -> Self {
127        RepairId(std::borrow::Cow::Owned(s))
128    }
129
130    pub fn as_str(&self) -> &str {
131        &self.0
132    }
133}
134
135impl fmt::Display for RepairId {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.0)
138    }
139}
140
141/// A structured repair proposal attached to a diagnostic.
142///
143/// `id` and `summary` are agent-readable metadata; `safety` is the
144/// dispatch dimension that decides whether the repair clears an
145/// autonomy ceiling. The concrete edits, when known statically, live on
146/// the diagnostic's `fix: Option<Vec<FixEdit>>`; this `Repair` is the
147/// classifier above those edits, not a replacement for them.
148#[derive(Debug, Clone)]
149pub struct Repair {
150    pub id: RepairId,
151    pub summary: String,
152    pub safety: RepairSafety,
153}
154
155impl Repair {
156    pub fn from_template(template: &RepairTemplate) -> Self {
157        Repair {
158            id: RepairId::from_static(template.id),
159            summary: template.summary.to_string(),
160            safety: template.safety,
161        }
162    }
163}
164
165/// Static-lifetime repair template bound to a diagnostic code.
166///
167/// Stored in the registry alongside `Code`. Construction sites can
168/// materialize a `Repair` via [`Repair::from_template`] or override
169/// `summary` for instance-specific detail by building a `Repair`
170/// directly.
171#[derive(Debug, Clone, Copy)]
172pub struct RepairTemplate {
173    pub id: &'static str,
174    pub summary: &'static str,
175    pub safety: RepairSafety,
176}
177
178impl Code {
179    /// Look up the default repair template attached to this diagnostic
180    /// code, or `None` if no actionable fix shape is registered.
181    pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
182        match self {
183            // --- TYP: type mismatches & coercions -------------------------
184            Code::TypeMismatch
185            | Code::ReturnTypeMismatch
186            | Code::AssignmentTypeMismatch
187            | Code::ArgumentTypeMismatch
188            | Code::VariableTypeMismatch
189            | Code::ClosureReturnTypeMismatch
190            | Code::FieldTypeMismatch
191            | Code::MethodTypeMismatch
192            | Code::InvalidIndexType => Some(&REPAIR_INSERT_EXPLICIT_CONVERSION),
193            Code::StringInterpolationRewrite => Some(&REPAIR_REWRITE_STRING_INTERPOLATION),
194            Code::UnknownTypeName => Some(&REPAIR_IMPORTS_FIX_PATH),
195            Code::InvalidCast => Some(&REPAIR_CASTS_REMOVE_UNCHECKED),
196
197            // --- NAM / IMP: imports & names -------------------------------
198            Code::UndefinedVariable
199            | Code::UndefinedFunction
200            | Code::UnknownField
201            | Code::UnknownMethod
202            | Code::UnknownBuiltin
203            | Code::UnknownDeclaration => Some(&REPAIR_BINDINGS_RENAME_TO_CLOSEST),
204            Code::InvalidMainSignature => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM),
205            Code::DeprecatedFunction => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
206            Code::ModuleImportUnresolved | Code::ImportResolutionFailed => {
207                Some(&REPAIR_IMPORTS_FIX_PATH)
208            }
209            Code::ModuleImportUnused => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
210            Code::ModuleImportOrder => Some(&REPAIR_IMPORTS_REORDER),
211
212            // --- CAP / RCV: capabilities & error recovery -----------------
213            Code::CapabilityResultUnchecked => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
214            Code::CapabilityBindingInvalid => Some(&REPAIR_MANUAL_REVIEW_CAPABILITY),
215            Code::EffectInheritanceViolation => Some(&REPAIR_POLICY_NARROW_CHILD_EFFECTS),
216            Code::RescueOutsideFunction | Code::TryOutsideFunction => {
217                Some(&REPAIR_ERRORS_WRAP_IN_FN)
218            }
219
220            // --- LLM / PRM: model + prompt contract -----------------------
221            Code::DeprecatedLlmOption => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
222            Code::LlmSchemaMissing => Some(&REPAIR_LLM_ADD_SCHEMA),
223            Code::LlmProviderIdentityBranch | Code::PromptProviderIdentityBranch => {
224                Some(&REPAIR_LLM_USE_CAPABILITY_FLAG)
225            }
226            Code::PromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
227            Code::PromptToolSurfaceUnknown | Code::PromptToolSurfaceDeferredReference => {
228                Some(&REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE)
229            }
230            Code::PromptVariantExplosion => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
231
232            // --- STD: stdlib usage ----------------------------------------
233            Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
234            Code::LintMissingStdlibMetadata => Some(&REPAIR_DOC_ADD_STDLIB_METADATA),
235
236            // --- OWN: ownership & mutability ------------------------------
237            Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
238            Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
239
240            // --- MAT: match exhaustiveness --------------------------------
241            Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
242            Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
243
244            // --- ORC: orchestration ---------------------------------------
245            Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
246
247            // --- FMT: formatter -------------------------------------------
248            Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
249                Some(&REPAIR_FORMAT_REFORMAT)
250            }
251
252            // --- LNT: lints with structured fixes -------------------------
253            Code::LintUnusedVariable
254            | Code::LintUnusedPatternBinding
255            | Code::LintUnusedParameter => Some(&REPAIR_BINDINGS_RENAME_UNUSED),
256            Code::LintCapabilityParameterName => Some(&REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER),
257            Code::LintUnusedImport => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
258            Code::LintUnusedFunction | Code::LintUnusedType => {
259                Some(&REPAIR_DECLARATIONS_REMOVE_UNUSED)
260            }
261            Code::LintMutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
262            Code::LintImportOrder => Some(&REPAIR_IMPORTS_REORDER),
263            Code::LintBlankLineBetweenItems
264            | Code::LintTrailingComma
265            | Code::LintUnnecessaryParentheses
266            | Code::LintRequireFileHeader => Some(&REPAIR_FORMAT_REFORMAT),
267            Code::LintLegacyDocComment => Some(&REPAIR_DOC_COMMENT_MIGRATE),
268            Code::LintEmptyBlock => Some(&REPAIR_BLOCK_REMOVE_EMPTY),
269            Code::LintUnnecessaryElseReturn | Code::LintLetThenReturn => {
270                Some(&REPAIR_CONTROL_FLOW_FLATTEN)
271            }
272            Code::LintNilCoalesceNoop
273            | Code::LintNilCoalesceSelfFallback
274            | Code::LintRedundantNilTernary
275            | Code::LintUnnecessarySafeNavigation
276            | Code::LintUnnecessaryNonNullAssert
277            | Code::LintPreferOptionalShorthand
278            | Code::LintComparisonToBool
279            | Code::LintPointlessComparison
280            | Code::LintConstantLogicalOperand => Some(&REPAIR_EXPRESSION_SIMPLIFY),
281            Code::LintUnnecessaryCast => Some(&REPAIR_CASTS_REMOVE_REDUNDANT),
282            Code::LintRedundantClone => Some(&REPAIR_CLONE_REMOVE_REDUNDANT),
283            Code::LintEagerCollectionConversion => Some(&REPAIR_COLLECTION_PREFER_LAZY),
284            Code::LintDeadCodeAfterReturn => Some(&REPAIR_DEAD_CODE_REMOVE),
285            Code::LintRenamedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
286            Code::LintAmbientClockBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_CLOCK),
287            Code::LintAmbientFsBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_FS),
288            Code::LintAmbientEnvBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_ENV),
289            Code::LintAmbientRandomBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_RANDOM),
290            Code::LintAmbientNetBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NET),
291            Code::LintAmbientStdioBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS),
292            Code::LintAmbientHarnessMethod => Some(&REPAIR_BINDINGS_THREAD_HARNESS_METHOD),
293            Code::LintBroadHarnessParameter => Some(&REPAIR_BINDINGS_ATTENUATE_HARNESS),
294            Code::LintDeprecatedLlmOptions => Some(&REPAIR_LLM_MIGRATE_DEPRECATED_OPTION),
295            Code::LintTemplateProviderIdentityBranch => Some(&REPAIR_LLM_USE_CAPABILITY_FLAG),
296            Code::LintPromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
297            Code::LintShadowVariable => Some(&REPAIR_BINDINGS_RENAME_SHADOW),
298            Code::LintNamingConvention => Some(&REPAIR_STYLE_RENAME_TO_CONVENTION),
299            Code::LintUnhandledApprovalResult => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
300            Code::LintMissingHarndoc => Some(&REPAIR_DOC_ADD_HARNDOC),
301            Code::LintDuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
302            // HARN-LNT-029 is the lint face of the boundary-validation rule, so
303            // its repair has to validate. HARN-LNT-060 below is a different
304            // complaint — an inline options dict bypassing the typed option
305            // constructors — where naming the shape is the whole fix and no
306            // untrusted payload is involved.
307            Code::LintUntypedDictAccess => Some(&REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE),
308            Code::LintUnnormalizedOptions => Some(&REPAIR_TYPES_ADD_SHAPE_ANNOTATION),
309            Code::LintMcpToolAnnotations => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
310            Code::LintTemplateVariantExplosion | Code::LintLongRunningWithoutCleanup => {
311                Some(&REPAIR_MANUAL_NEEDS_HUMAN)
312            }
313
314            // Everything else: no statically known repair shape. Agents
315            // should treat these as "diagnose only" until a repair is
316            // registered.
317            _ => None,
318        }
319    }
320}
321
322// Repair-id catalog. Each `RepairTemplate` carries a kebab-case
323// namespaced id (`<namespace>/<verb-noun>`), a one-line summary written
324// in the imperative voice, and a `RepairSafety` class.
325//
326// Conventions:
327//   - Namespaces stay short: `bindings/`, `imports/`, `errors/`, `casts/`,
328//     `format/`, `llm/`, `prompts/`, `match/`, `stdlib/`, `lint/`,
329//     `doc/`, `style/`, `types/`, `manual/`.
330//   - Summary starts with a verb ("Replace…", "Remove…", "Insert…").
331//   - Safety must be the most permissive class that is still always true
332//     for every site this template attaches to. When unsure, pick the
333//     stricter class — agents tighten too-loose policies later, never
334//     too-tight ones.
335
336const REPAIR_INSERT_EXPLICIT_CONVERSION: RepairTemplate = RepairTemplate {
337    id: "casts/insert-explicit-conversion",
338    summary: "Insert an explicit conversion or correct the operand type",
339    safety: RepairSafety::ScopeLocal,
340};
341
342const REPAIR_REWRITE_STRING_INTERPOLATION: RepairTemplate = RepairTemplate {
343    id: "style/string-interpolation",
344    summary: "Rewrite string concatenation as an interpolation literal",
345    safety: RepairSafety::BehaviorPreserving,
346};
347
348const REPAIR_CASTS_REMOVE_UNCHECKED: RepairTemplate = RepairTemplate {
349    id: "casts/remove-unchecked",
350    summary: "Remove the unchecked cast or guard it with a type test",
351    safety: RepairSafety::ScopeLocal,
352};
353
354const REPAIR_CASTS_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
355    id: "casts/remove-redundant",
356    summary: "Remove the redundant cast",
357    safety: RepairSafety::BehaviorPreserving,
358};
359
360const REPAIR_BINDINGS_RENAME_TO_CLOSEST: RepairTemplate = RepairTemplate {
361    id: "bindings/rename-to-closest",
362    summary: "Rename to the closest in-scope identifier",
363    safety: RepairSafety::ScopeLocal,
364};
365
366const REPAIR_BINDINGS_MAKE_MUTABLE: RepairTemplate = RepairTemplate {
367    id: "bindings/make-mutable",
368    summary: "Declare the binding with `let` so it can be reassigned",
369    safety: RepairSafety::ScopeLocal,
370};
371
372const REPAIR_BINDINGS_MAKE_IMMUTABLE: RepairTemplate = RepairTemplate {
373    id: "bindings/make-immutable",
374    summary: "Declare the never-reassigned binding with `const` instead of `let`",
375    safety: RepairSafety::BehaviorPreserving,
376};
377
378const REPAIR_BINDINGS_RENAME_UNUSED: RepairTemplate = RepairTemplate {
379    id: "bindings/rename-unused",
380    summary: "Use the `_` discard binding for an unused binding",
381    safety: RepairSafety::BehaviorPreserving,
382};
383
384const REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER: RepairTemplate = RepairTemplate {
385    id: "bindings/name-capability-parameter",
386    summary: "Rename the capability parameter and its references after the capability it carries",
387    safety: RepairSafety::SurfaceChanging,
388};
389
390const REPAIR_BINDINGS_RENAME_SHADOW: RepairTemplate = RepairTemplate {
391    id: "bindings/rename-shadow",
392    summary: "Rename the shadowing binding to a distinct name",
393    safety: RepairSafety::ScopeLocal,
394};
395
396const REPAIR_BINDINGS_THREAD_HARNESS: RepairTemplate = RepairTemplate {
397    id: "bindings/thread-harness",
398    summary: "Thread the existing `harness` binding through local helper calls and replace the ambient stdio builtin with `harness.stdio.*`",
399    safety: RepairSafety::ScopeLocal,
400};
401
402const REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM: RepairTemplate = RepairTemplate {
403    id: "bindings/thread-harness-needs-param",
404    summary: "Add a `harness: Harness` parameter where the stdio capability handle is required and update local callers",
405    safety: RepairSafety::SurfaceChanging,
406};
407
408const REPAIR_BINDINGS_THREAD_HARNESS_METHOD: RepairTemplate = RepairTemplate {
409    id: "bindings/thread-harness-method",
410    summary: "Replace the ambient runtime builtin with its typed `harness.*` method and thread authority through local callers",
411    safety: RepairSafety::ScopeLocal,
412};
413
414const REPAIR_BINDINGS_THREAD_HARNESS_CLOCK: RepairTemplate = RepairTemplate {
415    id: "bindings/thread-harness-clock",
416    summary: "Replace the ambient clock builtin with the corresponding `harness.clock.*` method",
417    safety: RepairSafety::ScopeLocal,
418};
419
420const REPAIR_BINDINGS_THREAD_HARNESS_FS: RepairTemplate = RepairTemplate {
421    id: "bindings/thread-harness-fs",
422    summary: "Replace the ambient fs builtin with the corresponding `harness.fs.*` method",
423    safety: RepairSafety::ScopeLocal,
424};
425
426const REPAIR_BINDINGS_THREAD_HARNESS_ENV: RepairTemplate = RepairTemplate {
427    id: "bindings/thread-harness-env",
428    summary: "Replace the ambient env builtin with the corresponding `harness.env.*` method",
429    safety: RepairSafety::ScopeLocal,
430};
431
432const REPAIR_BINDINGS_THREAD_HARNESS_RANDOM: RepairTemplate = RepairTemplate {
433    id: "bindings/thread-harness-random",
434    summary: "Replace the ambient random builtin with the corresponding `harness.random.*` method",
435    safety: RepairSafety::ScopeLocal,
436};
437
438const REPAIR_BINDINGS_THREAD_HARNESS_NET: RepairTemplate = RepairTemplate {
439    id: "bindings/thread-harness-net",
440    summary: "Replace the ambient net builtin with the corresponding `harness.net.*` method",
441    safety: RepairSafety::ScopeLocal,
442};
443
444const REPAIR_DECLARATIONS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
445    id: "declarations/remove-unused",
446    summary: "Remove the unused declaration",
447    safety: RepairSafety::SurfaceChanging,
448};
449
450const REPAIR_IMPORTS_FIX_PATH: RepairTemplate = RepairTemplate {
451    id: "imports/fix-path",
452    summary: "Replace the import path with a resolvable target",
453    safety: RepairSafety::ScopeLocal,
454};
455
456const REPAIR_IMPORTS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
457    id: "imports/remove-unused",
458    summary: "Remove the unused import",
459    safety: RepairSafety::BehaviorPreserving,
460};
461
462const REPAIR_IMPORTS_REORDER: RepairTemplate = RepairTemplate {
463    id: "imports/reorder",
464    summary: "Reorder imports into canonical grouping",
465    safety: RepairSafety::FormatOnly,
466};
467
468const REPAIR_ERRORS_CHECK_OR_RESCUE: RepairTemplate = RepairTemplate {
469    id: "errors/check-or-rescue",
470    summary: "Check the result or wrap the call in a `rescue` block",
471    safety: RepairSafety::ScopeLocal,
472};
473
474const REPAIR_ERRORS_WRAP_IN_FN: RepairTemplate = RepairTemplate {
475    id: "errors/wrap-in-fn",
476    summary: "Move the construct inside a function body",
477    safety: RepairSafety::SurfaceChanging,
478};
479
480const REPAIR_MATCH_ADD_MISSING_ARMS: RepairTemplate = RepairTemplate {
481    id: "match/add-missing-arms",
482    summary: "Add arms covering the missing variants",
483    safety: RepairSafety::ScopeLocal,
484};
485
486const REPAIR_MATCH_REMOVE_DUPLICATE_ARM: RepairTemplate = RepairTemplate {
487    id: "match/remove-duplicate-arm",
488    summary: "Remove the duplicated match arm",
489    safety: RepairSafety::BehaviorPreserving,
490};
491
492const REPAIR_FORMAT_REFORMAT: RepairTemplate = RepairTemplate {
493    id: "format/reformat",
494    summary: "Apply canonical formatting",
495    safety: RepairSafety::FormatOnly,
496};
497
498const REPAIR_DOC_COMMENT_MIGRATE: RepairTemplate = RepairTemplate {
499    id: "doc/migrate-comment-style",
500    summary: "Migrate the legacy comment to canonical doc syntax",
501    safety: RepairSafety::FormatOnly,
502};
503
504const REPAIR_DOC_ADD_HARNDOC: RepairTemplate = RepairTemplate {
505    id: "doc/add-harndoc",
506    summary: "Add a `///` doc comment describing this declaration",
507    safety: RepairSafety::BehaviorPreserving,
508};
509
510const REPAIR_DOC_ADD_STDLIB_METADATA: RepairTemplate = RepairTemplate {
511    id: "doc/add-stdlib-metadata",
512    summary: "Add `@effects` and `@errors` fields to the stdlib function's doc block",
513    safety: RepairSafety::BehaviorPreserving,
514};
515
516const REPAIR_BLOCK_REMOVE_EMPTY: RepairTemplate = RepairTemplate {
517    id: "blocks/remove-empty",
518    summary: "Remove the empty block or fill in an explicit body",
519    safety: RepairSafety::ScopeLocal,
520};
521
522const REPAIR_CONTROL_FLOW_FLATTEN: RepairTemplate = RepairTemplate {
523    id: "control-flow/flatten",
524    summary: "Flatten the unnecessary control flow construct",
525    safety: RepairSafety::BehaviorPreserving,
526};
527
528const REPAIR_EXPRESSION_SIMPLIFY: RepairTemplate = RepairTemplate {
529    id: "expressions/simplify",
530    summary: "Simplify the expression to its canonical form",
531    safety: RepairSafety::BehaviorPreserving,
532};
533
534const REPAIR_CLONE_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
535    id: "clones/remove-redundant",
536    summary: "Remove the redundant clone",
537    safety: RepairSafety::BehaviorPreserving,
538};
539
540const REPAIR_COLLECTION_PREFER_LAZY: RepairTemplate = RepairTemplate {
541    id: "collections/prefer-lazy",
542    summary: "Replace the eager collection step with a lazy variant",
543    safety: RepairSafety::ScopeLocal,
544};
545
546const REPAIR_DEAD_CODE_REMOVE: RepairTemplate = RepairTemplate {
547    id: "control-flow/remove-dead",
548    summary: "Remove the unreachable code",
549    safety: RepairSafety::BehaviorPreserving,
550};
551
552const REPAIR_STDLIB_MIGRATE_RENAMED: RepairTemplate = RepairTemplate {
553    id: "stdlib/migrate-renamed",
554    summary: "Rename the call to the renamed stdlib symbol",
555    safety: RepairSafety::ScopeLocal,
556};
557
558const REPAIR_BINDINGS_ATTENUATE_HARNESS: RepairTemplate = RepairTemplate {
559    id: "bindings/attenuate-harness",
560    summary: "Replace the root Harness parameter with the single capability the helper uses",
561    safety: RepairSafety::SurfaceChanging,
562};
563
564const REPAIR_LLM_MIGRATE_DEPRECATED_OPTION: RepairTemplate = RepairTemplate {
565    id: "llm/migrate-deprecated-option",
566    summary: "Replace the deprecated option with its supported equivalent",
567    safety: RepairSafety::ScopeLocal,
568};
569
570const REPAIR_LLM_ADD_SCHEMA: RepairTemplate = RepairTemplate {
571    id: "llm/add-schema",
572    summary: "Add a typed output schema to the LLM call",
573    safety: RepairSafety::SurfaceChanging,
574};
575
576const REPAIR_LLM_USE_CAPABILITY_FLAG: RepairTemplate = RepairTemplate {
577    id: "llm/use-capability-flag",
578    summary: "Branch on a capability flag instead of provider identity",
579    safety: RepairSafety::CapabilityChanging,
580};
581
582const REPAIR_PROMPTS_ESCAPE_INJECTION: RepairTemplate = RepairTemplate {
583    id: "prompts/escape-injection",
584    summary: "Pass the untrusted input through a structured placeholder",
585    safety: RepairSafety::ScopeLocal,
586};
587
588const REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE: RepairTemplate = RepairTemplate {
589    id: "prompts/add-tool-to-surface",
590    summary: "Add the referenced tool to the declared tool surface",
591    safety: RepairSafety::SurfaceChanging,
592};
593
594const REPAIR_STYLE_RENAME_TO_CONVENTION: RepairTemplate = RepairTemplate {
595    id: "style/rename-to-convention",
596    summary: "Rename to match the casing convention for this kind",
597    safety: RepairSafety::SurfaceChanging,
598};
599
600const REPAIR_TYPES_ADD_SHAPE_ANNOTATION: RepairTemplate = RepairTemplate {
601    id: "types/add-shape-annotation",
602    summary: "Annotate the dict with a concrete shape type",
603    safety: RepairSafety::SurfaceChanging,
604};
605
606/// The repair for a boundary value read without validation.
607///
608/// Since harn#6252 a binding annotation *is* enforced, so annotating a
609/// `json_parse` result does validate it — the original reason for keeping
610/// `types/add-shape-annotation` away from this rule (that the annotation was
611/// erased, and so removed the diagnostic without removing the hazard) no longer
612/// holds.
613///
614/// This still offers schema validation rather than the annotation, for a
615/// different and narrower reason: a binding assertion reports the declared type
616/// and the value's kind, while `schema_expect` reports which field failed and
617/// why. For an untrusted payload that difference is the whole diagnosis. The
618/// annotation is now a correct fix; it is not the most informative one, and a
619/// one-step automated repair should offer the most informative.
620const REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE: RepairTemplate = RepairTemplate {
621    id: "types/validate-boundary-value",
622    summary: "Validate the parsed value with schema_expect() or schema_check() before reading it",
623    safety: RepairSafety::ScopeLocal,
624};
625
626const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
627    id: "manual/review-capability-binding",
628    summary: "Review the capability binding; the fix is not mechanical",
629    safety: RepairSafety::NeedsHuman,
630};
631
632const REPAIR_POLICY_NARROW_CHILD_EFFECTS: RepairTemplate = RepairTemplate {
633    id: "policy/narrow-child-effects",
634    summary: "Narrow the child agent's effects to a subset of the parent's, or widen the parent's declared effects",
635    safety: RepairSafety::SurfaceChanging,
636};
637
638const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
639    id: "manual/needs-human",
640    summary: "Plan a human-led change; auto-apply is not safe here",
641    safety: RepairSafety::NeedsHuman,
642};
643
644/// Every repair template registered by [`Code::repair_template`], in source
645/// order for catalog generation and health checks.
646pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
647    &REPAIR_INSERT_EXPLICIT_CONVERSION,
648    &REPAIR_REWRITE_STRING_INTERPOLATION,
649    &REPAIR_CASTS_REMOVE_UNCHECKED,
650    &REPAIR_CASTS_REMOVE_REDUNDANT,
651    &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
652    &REPAIR_BINDINGS_MAKE_MUTABLE,
653    &REPAIR_BINDINGS_MAKE_IMMUTABLE,
654    &REPAIR_BINDINGS_RENAME_UNUSED,
655    &REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER,
656    &REPAIR_BINDINGS_RENAME_SHADOW,
657    &REPAIR_BINDINGS_THREAD_HARNESS,
658    &REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM,
659    &REPAIR_BINDINGS_THREAD_HARNESS_METHOD,
660    &REPAIR_BINDINGS_THREAD_HARNESS_CLOCK,
661    &REPAIR_BINDINGS_THREAD_HARNESS_FS,
662    &REPAIR_BINDINGS_THREAD_HARNESS_ENV,
663    &REPAIR_BINDINGS_THREAD_HARNESS_RANDOM,
664    &REPAIR_BINDINGS_THREAD_HARNESS_NET,
665    &REPAIR_DECLARATIONS_REMOVE_UNUSED,
666    &REPAIR_IMPORTS_FIX_PATH,
667    &REPAIR_IMPORTS_REMOVE_UNUSED,
668    &REPAIR_IMPORTS_REORDER,
669    &REPAIR_ERRORS_CHECK_OR_RESCUE,
670    &REPAIR_ERRORS_WRAP_IN_FN,
671    &REPAIR_MATCH_ADD_MISSING_ARMS,
672    &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
673    &REPAIR_FORMAT_REFORMAT,
674    &REPAIR_DOC_COMMENT_MIGRATE,
675    &REPAIR_DOC_ADD_HARNDOC,
676    &REPAIR_DOC_ADD_STDLIB_METADATA,
677    &REPAIR_BLOCK_REMOVE_EMPTY,
678    &REPAIR_CONTROL_FLOW_FLATTEN,
679    &REPAIR_EXPRESSION_SIMPLIFY,
680    &REPAIR_CLONE_REMOVE_REDUNDANT,
681    &REPAIR_COLLECTION_PREFER_LAZY,
682    &REPAIR_DEAD_CODE_REMOVE,
683    &REPAIR_STDLIB_MIGRATE_RENAMED,
684    &REPAIR_BINDINGS_ATTENUATE_HARNESS,
685    &REPAIR_LLM_MIGRATE_DEPRECATED_OPTION,
686    &REPAIR_LLM_ADD_SCHEMA,
687    &REPAIR_LLM_USE_CAPABILITY_FLAG,
688    &REPAIR_PROMPTS_ESCAPE_INJECTION,
689    &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
690    &REPAIR_STYLE_RENAME_TO_CONVENTION,
691    &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
692    &REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE,
693    &REPAIR_MANUAL_REVIEW_CAPABILITY,
694    &REPAIR_MANUAL_NEEDS_HUMAN,
695    &REPAIR_POLICY_NARROW_CHILD_EFFECTS,
696];
697
698#[cfg(test)]
699mod tests {
700    use super::super::Category;
701    use super::{Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
702    use std::collections::HashSet;
703    use std::str::FromStr;
704
705    #[test]
706    fn parses_registered_code() {
707        assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
708    }
709
710    #[test]
711    fn registry_has_unique_identifiers() {
712        let mut seen = HashSet::new();
713        for entry in Code::registry() {
714            assert!(
715                seen.insert(entry.identifier),
716                "duplicate diagnostic code {}",
717                entry.identifier
718            );
719            assert_eq!(entry.code.as_str(), entry.identifier);
720            assert_eq!(entry.code.category(), entry.category);
721            let expected_prefix = format!("HARN-{}-", entry.category);
722            assert!(entry.identifier.starts_with(&expected_prefix));
723            let suffix = entry.identifier.trim_start_matches(&expected_prefix);
724            assert_eq!(suffix.len(), 3);
725            assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
726            assert!(!entry.summary.is_empty());
727        }
728        assert!(Code::registry().len() >= 40);
729    }
730
731    #[test]
732    fn every_category_is_populated() {
733        for category in Category::ALL {
734            assert!(
735                Code::registry()
736                    .iter()
737                    .any(|entry| entry.category == *category),
738                "missing diagnostic code category {category}"
739            );
740        }
741    }
742
743    #[test]
744    fn every_code_has_non_empty_explanation() {
745        for entry in Code::registry() {
746            let body = entry.code.explanation();
747            assert!(
748                !body.trim().is_empty(),
749                "diagnostic code {} has an empty explanation file",
750                entry.identifier
751            );
752            assert!(
753                body.contains(entry.identifier),
754                "explanation for {} should reference its identifier",
755                entry.identifier
756            );
757        }
758    }
759
760    #[test]
761    fn related_codes_are_registered_and_non_self() {
762        for entry in Code::registry() {
763            for &other in entry.code.related() {
764                assert_ne!(
765                    other, entry.code,
766                    "{} lists itself as a related code",
767                    entry.identifier
768                );
769                assert!(
770                    Code::registry().iter().any(|e| e.code == other),
771                    "{} lists unregistered related code {}",
772                    entry.identifier,
773                    other
774                );
775            }
776        }
777    }
778
779    #[test]
780    fn repair_safety_string_roundtrip() {
781        for safety in RepairSafety::ALL {
782            let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
783            assert_eq!(parsed, *safety);
784            assert_eq!(parsed.to_string(), safety.as_str());
785        }
786        assert_eq!(
787            RepairSafety::from_str("not-a-safety-class"),
788            Err(ParseRepairSafetyError)
789        );
790    }
791
792    #[test]
793    fn repair_safety_ordering_is_monotonic_low_to_high() {
794        // The is_at_most ceiling check relies on this ordering being
795        // least-to-most disruptive; a regression here flips the meaning
796        // of `harn fix --safety <ceiling>` for every caller.
797        let order = RepairSafety::ALL;
798        for window in order.windows(2) {
799            assert!(
800                window[0] < window[1],
801                "{:?} should be safer than {:?}",
802                window[0],
803                window[1]
804            );
805            assert!(window[0].is_at_most(window[1]));
806            assert!(!window[1].is_at_most(window[0]));
807        }
808    }
809
810    #[test]
811    fn repair_registry_has_at_least_twenty_entries() {
812        assert!(
813            REPAIR_REGISTRY.len() >= 20,
814            "expected ≥20 repair templates, found {}",
815            REPAIR_REGISTRY.len()
816        );
817    }
818
819    #[test]
820    fn repair_ids_are_kebab_case_namespaced_and_unique() {
821        let mut seen = HashSet::new();
822        for template in REPAIR_REGISTRY {
823            assert!(
824                seen.insert(template.id),
825                "duplicate repair id {}",
826                template.id
827            );
828            let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
829                panic!(
830                    "repair id `{}` is missing `<namespace>/` prefix",
831                    template.id
832                )
833            });
834            assert!(
835                !namespace.is_empty() && !leaf.is_empty(),
836                "repair id `{}` has empty namespace or leaf",
837                template.id
838            );
839            for ch in template.id.chars() {
840                assert!(
841                    ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
842                    "repair id `{}` has non-kebab character {ch:?}",
843                    template.id
844                );
845            }
846            assert!(
847                !template.summary.is_empty(),
848                "repair {} has empty summary",
849                template.id
850            );
851            // Summaries are imperative: start with a capital ASCII letter.
852            let first = template.summary.chars().next().unwrap();
853            assert!(
854                first.is_ascii_uppercase(),
855                "repair {} summary `{}` should start with a capital",
856                template.id,
857                template.summary
858            );
859        }
860    }
861
862    #[test]
863    fn manual_namespace_is_needs_human() {
864        for template in REPAIR_REGISTRY {
865            if let Some(("manual", _)) = template.id.split_once('/') {
866                assert_eq!(
867                    template.safety,
868                    RepairSafety::NeedsHuman,
869                    "manual/* repair {} must be NeedsHuman",
870                    template.id
871                );
872            }
873        }
874    }
875
876    #[test]
877    fn known_codes_carry_expected_safety_class() {
878        // Spot-check: the autonomy contract for several representative
879        // diagnostics. Lock in the safety class so cross-repo agents that
880        // dispatch on these don't silently drift when the catalog moves.
881        let expected: &[(Code, RepairSafety, &str)] = &[
882            (
883                Code::FormatterWouldReformat,
884                RepairSafety::FormatOnly,
885                "format/reformat",
886            ),
887            (
888                Code::ModuleImportUnused,
889                RepairSafety::BehaviorPreserving,
890                "imports/remove-unused",
891            ),
892            (
893                Code::ImmutableAssignment,
894                RepairSafety::ScopeLocal,
895                "bindings/make-mutable",
896            ),
897            (
898                Code::LintUnusedFunction,
899                RepairSafety::SurfaceChanging,
900                "declarations/remove-unused",
901            ),
902            (
903                Code::LlmProviderIdentityBranch,
904                RepairSafety::CapabilityChanging,
905                "llm/use-capability-flag",
906            ),
907            (
908                Code::PromptVariantExplosion,
909                RepairSafety::NeedsHuman,
910                "manual/needs-human",
911            ),
912            (
913                Code::NonExhaustiveMatch,
914                RepairSafety::ScopeLocal,
915                "match/add-missing-arms",
916            ),
917            (
918                Code::LintAmbientClockBuiltin,
919                RepairSafety::ScopeLocal,
920                "bindings/thread-harness-clock",
921            ),
922            (
923                Code::LintAmbientStdioBuiltin,
924                RepairSafety::ScopeLocal,
925                "bindings/thread-harness",
926            ),
927            (
928                Code::InvalidMainSignature,
929                RepairSafety::SurfaceChanging,
930                "bindings/thread-harness-needs-param",
931            ),
932        ];
933        for (code, safety, repair_id) in expected {
934            let template = code
935                .repair_template()
936                .unwrap_or_else(|| panic!("{code} should have a repair template"));
937            assert_eq!(template.safety, *safety, "{code} safety class drifted");
938            assert_eq!(template.id, *repair_id, "{code} repair id drifted");
939        }
940    }
941
942    #[test]
943    fn repair_templates_cover_at_least_twenty_codes() {
944        let covered = Code::ALL
945            .iter()
946            .filter(|code| code.repair_template().is_some())
947            .count();
948        assert!(
949            covered >= 20,
950            "expected ≥20 codes with a repair template, found {covered}"
951        );
952    }
953
954    /// A rule about validating a boundary value cannot be repaired by an
955    /// annotation.
956    ///
957    /// An annotation is erased before the value exists: annotating a
958    /// `json_parse` result reads an int out of a field declared `string` and
959    /// accepts an array as a record, with no diagnostic at either compile time
960    /// or run time. Offering it as the one-step repair for these codes would
961    /// auto-apply the escape the codes exist to close, and a repair is applied
962    /// more readily than help text is read (harn#6234).
963    ///
964    /// `LintUnnormalizedOptions` is deliberately not in this list. It is about
965    /// an inline options dict bypassing the typed option constructors, where no
966    /// untrusted payload is involved and naming the shape is the whole fix.
967    #[test]
968    fn boundary_validation_codes_offer_the_most_informative_repair() {
969        for code in [Code::BoundaryValueUnvalidated, Code::LintUntypedDictAccess] {
970            let Some(template) = code.repair_template() else {
971                continue;
972            };
973            assert_ne!(
974                template.id, "types/add-shape-annotation",
975                "{code:?} is a boundary-validation rule; an annotation validates the value \
976                 (harn#6252) but reports only the declared type and the value's kind, so the \
977                 offered one-step repair must be the one that names the failing field"
978            );
979        }
980    }
981
982    #[test]
983    fn every_registered_repair_is_referenced_by_some_code() {
984        let referenced: HashSet<&'static str> = Code::ALL
985            .iter()
986            .filter_map(|code| code.repair_template())
987            .map(|template| template.id)
988            .collect();
989        for template in REPAIR_REGISTRY {
990            assert!(
991                referenced.contains(template.id),
992                "repair {} is in REPAIR_REGISTRY but no Code maps to it",
993                template.id
994            );
995        }
996    }
997
998    #[test]
999    fn every_referenced_repair_template_is_in_registry() {
1000        let registered: HashSet<&'static str> =
1001            REPAIR_REGISTRY.iter().map(|template| template.id).collect();
1002        for code in Code::ALL {
1003            let Some(template) = code.repair_template() else {
1004                continue;
1005            };
1006            assert!(
1007                registered.contains(template.id),
1008                "repair {} (used by {}) is missing from REPAIR_REGISTRY",
1009                template.id,
1010                code
1011            );
1012        }
1013    }
1014}