1use std::{fmt, str::FromStr};
14
15use super::Code;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum RepairSafety {
28 FormatOnly,
31 BehaviorPreserving,
34 ScopeLocal,
38 SurfaceChanging,
41 CapabilityChanging,
45 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 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 pub const fn is_at_most(self, ceiling: RepairSafety) -> bool {
79 (self as u8) <= (ceiling as u8)
80 }
81
82 pub const fn is_machine_applicable(self) -> bool {
85 self.is_at_most(RepairSafety::BehaviorPreserving)
86 }
87}
88
89impl fmt::Display for RepairSafety {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct ParseRepairSafetyError;
98
99impl fmt::Display for ParseRepairSafetyError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str("unknown Harn repair-safety class")
102 }
103}
104
105impl std::error::Error for ParseRepairSafetyError {}
106
107impl FromStr for RepairSafety {
108 type Err = ParseRepairSafetyError;
109
110 fn from_str(value: &str) -> Result<Self, Self::Err> {
111 RepairSafety::ALL
112 .iter()
113 .copied()
114 .find(|safety| safety.as_str() == value)
115 .ok_or(ParseRepairSafetyError)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub struct RepairId(std::borrow::Cow<'static, str>);
126
127impl RepairId {
128 pub const fn from_static(s: &'static str) -> Self {
129 RepairId(std::borrow::Cow::Borrowed(s))
130 }
131
132 pub fn from_owned(s: String) -> Self {
133 RepairId(std::borrow::Cow::Owned(s))
134 }
135
136 pub fn as_str(&self) -> &str {
137 &self.0
138 }
139}
140
141impl fmt::Display for RepairId {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(&self.0)
144 }
145}
146
147#[derive(Debug, Clone)]
155pub struct Repair {
156 pub id: RepairId,
157 pub summary: String,
158 pub safety: RepairSafety,
159}
160
161impl Repair {
162 pub fn from_template(template: &RepairTemplate) -> Self {
163 Repair {
164 id: RepairId::from_static(template.id),
165 summary: template.summary.to_string(),
166 safety: template.safety,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy)]
178pub struct RepairTemplate {
179 pub id: &'static str,
180 pub summary: &'static str,
181 pub safety: RepairSafety,
182}
183
184impl Code {
185 pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
188 match self {
189 Code::TypeMismatch
191 | Code::ReturnTypeMismatch
192 | Code::AssignmentTypeMismatch
193 | Code::ArgumentTypeMismatch
194 | Code::VariableTypeMismatch
195 | Code::ClosureReturnTypeMismatch
196 | Code::FieldTypeMismatch
197 | Code::MethodTypeMismatch
198 | Code::InvalidIndexType => Some(&REPAIR_INSERT_EXPLICIT_CONVERSION),
199 Code::StringInterpolationRewrite => Some(&REPAIR_REWRITE_STRING_INTERPOLATION),
200 Code::UnknownTypeName => Some(&REPAIR_IMPORTS_FIX_PATH),
201 Code::ImplicitAnyParameter => Some(&REPAIR_TYPES_ANNOTATE_PARAMETER),
202 Code::InvalidCast => Some(&REPAIR_CASTS_REMOVE_UNCHECKED),
203
204 Code::UndefinedVariable
206 | Code::UndefinedFunction
207 | Code::UnknownField
208 | Code::UnknownMethod
209 | Code::UnknownBuiltin
210 | Code::UnknownDeclaration => Some(&REPAIR_BINDINGS_RENAME_TO_CLOSEST),
211 Code::InvalidMainSignature => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM),
212 Code::DeprecatedFunction => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
213 Code::ModuleImportUnresolved | Code::ImportResolutionFailed => {
214 Some(&REPAIR_IMPORTS_FIX_PATH)
215 }
216 Code::ModuleImportUnused => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
217 Code::ModuleImportOrder => Some(&REPAIR_IMPORTS_REORDER),
218
219 Code::CapabilityResultUnchecked => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
221 Code::CapabilityBindingInvalid => Some(&REPAIR_MANUAL_REVIEW_CAPABILITY),
222 Code::EffectInheritanceViolation => Some(&REPAIR_POLICY_NARROW_CHILD_EFFECTS),
223 Code::RescueOutsideFunction | Code::TryOutsideFunction => {
224 Some(&REPAIR_ERRORS_WRAP_IN_FN)
225 }
226
227 Code::LlmSchemaMissing => Some(&REPAIR_LLM_ADD_SCHEMA),
229 Code::LlmProviderIdentityBranch | Code::PromptProviderIdentityBranch => {
230 Some(&REPAIR_LLM_USE_CAPABILITY_FLAG)
231 }
232 Code::PromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
233 Code::PromptToolSurfaceUnknown | Code::PromptToolSurfaceDeferredReference => {
234 Some(&REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE)
235 }
236 Code::PromptVariantExplosion => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
237
238 Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
240 Code::LintMissingStdlibMetadata => Some(&REPAIR_DOC_ADD_STDLIB_METADATA),
241
242 Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
244 Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
245
246 Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
248 Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
249
250 Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
252
253 Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
255 Some(&REPAIR_FORMAT_REFORMAT)
256 }
257
258 Code::LintUnusedVariable
260 | Code::LintUnusedPatternBinding
261 | Code::LintUnusedParameter => Some(&REPAIR_BINDINGS_RENAME_UNUSED),
262 Code::LintUnusedPipelineInput => Some(&REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT),
263 Code::LintCapabilityParameterName => Some(&REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER),
264 Code::LintUnusedImport => Some(&REPAIR_IMPORTS_REMOVE_UNUSED),
265 Code::LintUnusedFunction | Code::LintUnusedType => {
266 Some(&REPAIR_DECLARATIONS_REMOVE_UNUSED)
267 }
268 Code::LintMutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
269 Code::LintImportOrder => Some(&REPAIR_IMPORTS_REORDER),
270 Code::LintBlankLineBetweenItems
271 | Code::LintTrailingComma
272 | Code::LintUnnecessaryParentheses
273 | Code::LintRequireFileHeader => Some(&REPAIR_FORMAT_REFORMAT),
274 Code::LintLegacyDocComment => Some(&REPAIR_DOC_COMMENT_MIGRATE),
275 Code::LintEmptyBlock => Some(&REPAIR_BLOCK_REMOVE_EMPTY),
276 Code::LintUnnecessaryElseReturn | Code::LintLetThenReturn => {
277 Some(&REPAIR_CONTROL_FLOW_FLATTEN)
278 }
279 Code::LintNilCoalesceNoop
280 | Code::LintNilCoalesceSelfFallback
281 | Code::LintRedundantNilTernary
282 | Code::LintUnnecessarySafeNavigation
283 | Code::LintUnnecessaryNonNullAssert
284 | Code::LintPreferOptionalShorthand
285 | Code::LintComparisonToBool
286 | Code::LintPointlessComparison
287 | Code::LintConstantLogicalOperand => Some(&REPAIR_EXPRESSION_SIMPLIFY),
288 Code::LintPreferPick => Some(&REPAIR_RECORDS_PICK_FIELDS),
289 Code::LintUnnecessaryCast => Some(&REPAIR_CASTS_REMOVE_REDUNDANT),
290 Code::LintRedundantClone => Some(&REPAIR_CLONE_REMOVE_REDUNDANT),
291 Code::LintEagerCollectionConversion => Some(&REPAIR_COLLECTION_PREFER_LAZY),
292 Code::LintDeadCodeAfterReturn => Some(&REPAIR_DEAD_CODE_REMOVE),
293 Code::LintRenamedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
294 Code::LintAmbientClockBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_CLOCK),
295 Code::LintAmbientFsBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_FS),
296 Code::LintAmbientEnvBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_ENV),
297 Code::LintAmbientRandomBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_RANDOM),
298 Code::LintAmbientNetBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS_NET),
299 Code::LintAmbientStdioBuiltin => Some(&REPAIR_BINDINGS_THREAD_HARNESS),
300 Code::LintAmbientHarnessMethod => Some(&REPAIR_BINDINGS_THREAD_HARNESS_METHOD),
301 Code::LintBroadHarnessParameter => Some(&REPAIR_BINDINGS_ATTENUATE_HARNESS),
302 Code::LintRemovedLlmOptions => Some(&REPAIR_LLM_MIGRATE_REMOVED_OPTION),
303 Code::LintTemplateProviderIdentityBranch => Some(&REPAIR_LLM_USE_CAPABILITY_FLAG),
304 Code::LintPromptInjectionRisk => Some(&REPAIR_PROMPTS_ESCAPE_INJECTION),
305 Code::LintShadowVariable => Some(&REPAIR_BINDINGS_RENAME_SHADOW),
306 Code::LintNamingConvention => Some(&REPAIR_STYLE_RENAME_TO_CONVENTION),
307 Code::LintUnhandledApprovalResult => Some(&REPAIR_ERRORS_CHECK_OR_RESCUE),
308 Code::LintMissingHarndoc => Some(&REPAIR_DOC_ADD_HARNDOC),
309 Code::LintDuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
310 Code::LintUntypedDictAccess => Some(&REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE),
316 Code::LintUnnormalizedOptions => Some(&REPAIR_TYPES_ADD_SHAPE_ANNOTATION),
317 Code::LintMcpToolAnnotations => Some(&REPAIR_MANUAL_NEEDS_HUMAN),
318 Code::LintTemplateVariantExplosion | Code::LintLongRunningWithoutCleanup => {
319 Some(&REPAIR_MANUAL_NEEDS_HUMAN)
320 }
321
322 _ => None,
326 }
327 }
328}
329
330const REPAIR_INSERT_EXPLICIT_CONVERSION: RepairTemplate = RepairTemplate {
345 id: "casts/insert-explicit-conversion",
346 summary: "Insert an explicit conversion or correct the operand type",
347 safety: RepairSafety::ScopeLocal,
348};
349
350const REPAIR_REWRITE_STRING_INTERPOLATION: RepairTemplate = RepairTemplate {
351 id: "style/string-interpolation",
352 summary: "Rewrite string concatenation as an interpolation literal",
353 safety: RepairSafety::BehaviorPreserving,
354};
355
356const REPAIR_CASTS_REMOVE_UNCHECKED: RepairTemplate = RepairTemplate {
357 id: "casts/remove-unchecked",
358 summary: "Remove the unchecked cast or guard it with a type test",
359 safety: RepairSafety::ScopeLocal,
360};
361
362const REPAIR_CASTS_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
363 id: "casts/remove-redundant",
364 summary: "Remove the redundant cast",
365 safety: RepairSafety::BehaviorPreserving,
366};
367
368const REPAIR_BINDINGS_RENAME_TO_CLOSEST: RepairTemplate = RepairTemplate {
369 id: "bindings/rename-to-closest",
370 summary: "Rename to the closest in-scope identifier",
371 safety: RepairSafety::ScopeLocal,
372};
373
374const REPAIR_BINDINGS_MAKE_MUTABLE: RepairTemplate = RepairTemplate {
375 id: "bindings/make-mutable",
376 summary: "Declare the binding with `let` so it can be reassigned",
377 safety: RepairSafety::ScopeLocal,
378};
379
380const REPAIR_BINDINGS_MAKE_IMMUTABLE: RepairTemplate = RepairTemplate {
381 id: "bindings/make-immutable",
382 summary: "Declare the never-reassigned binding with `const` instead of `let`",
383 safety: RepairSafety::BehaviorPreserving,
384};
385
386const REPAIR_BINDINGS_RENAME_UNUSED: RepairTemplate = RepairTemplate {
387 id: "bindings/rename-unused",
388 summary: "Mark an unused binding without changing callable arity",
389 safety: RepairSafety::BehaviorPreserving,
390};
391
392const REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT: RepairTemplate = RepairTemplate {
393 id: "bindings/remove-unused-pipeline-input",
394 summary: "Remove an explicitly unused test pipeline input",
395 safety: RepairSafety::SurfaceChanging,
396};
397
398const REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER: RepairTemplate = RepairTemplate {
399 id: "bindings/name-capability-parameter",
400 summary: "Rename the capability parameter and its references after the capability it carries",
401 safety: RepairSafety::SurfaceChanging,
402};
403
404const REPAIR_BINDINGS_RENAME_SHADOW: RepairTemplate = RepairTemplate {
405 id: "bindings/rename-shadow",
406 summary: "Rename the shadowing binding to a distinct name",
407 safety: RepairSafety::ScopeLocal,
408};
409
410const REPAIR_BINDINGS_THREAD_HARNESS: RepairTemplate = RepairTemplate {
411 id: "bindings/thread-harness",
412 summary: "Thread the existing `harness` binding through local helper calls and replace the ambient stdio builtin with `harness.stdio.*`",
413 safety: RepairSafety::ScopeLocal,
414};
415
416const REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM: RepairTemplate = RepairTemplate {
417 id: "bindings/thread-harness-needs-param",
418 summary: "Add a `harness: Harness` parameter where the stdio capability handle is required and update local callers",
419 safety: RepairSafety::SurfaceChanging,
420};
421
422const REPAIR_BINDINGS_THREAD_HARNESS_METHOD: RepairTemplate = RepairTemplate {
423 id: "bindings/thread-harness-method",
424 summary: "Replace the ambient runtime builtin with its typed `harness.*` method and thread authority through local callers",
425 safety: RepairSafety::ScopeLocal,
426};
427
428const REPAIR_BINDINGS_THREAD_HARNESS_CLOCK: RepairTemplate = RepairTemplate {
429 id: "bindings/thread-harness-clock",
430 summary: "Replace the ambient clock builtin with the corresponding `harness.clock.*` method",
431 safety: RepairSafety::ScopeLocal,
432};
433
434const REPAIR_BINDINGS_THREAD_HARNESS_FS: RepairTemplate = RepairTemplate {
435 id: "bindings/thread-harness-fs",
436 summary: "Replace the ambient fs builtin with the corresponding `harness.fs.*` method",
437 safety: RepairSafety::ScopeLocal,
438};
439
440const REPAIR_BINDINGS_THREAD_HARNESS_ENV: RepairTemplate = RepairTemplate {
441 id: "bindings/thread-harness-env",
442 summary: "Replace the ambient env builtin with the corresponding `harness.env.*` method",
443 safety: RepairSafety::ScopeLocal,
444};
445
446const REPAIR_BINDINGS_THREAD_HARNESS_RANDOM: RepairTemplate = RepairTemplate {
447 id: "bindings/thread-harness-random",
448 summary: "Replace the ambient random builtin with the corresponding `harness.random.*` method",
449 safety: RepairSafety::ScopeLocal,
450};
451
452const REPAIR_BINDINGS_THREAD_HARNESS_NET: RepairTemplate = RepairTemplate {
453 id: "bindings/thread-harness-net",
454 summary: "Replace the ambient net builtin with the corresponding `harness.net.*` method",
455 safety: RepairSafety::ScopeLocal,
456};
457
458const REPAIR_DECLARATIONS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
459 id: "declarations/remove-unused",
460 summary: "Remove the unused declaration",
461 safety: RepairSafety::SurfaceChanging,
462};
463
464const REPAIR_IMPORTS_FIX_PATH: RepairTemplate = RepairTemplate {
465 id: "imports/fix-path",
466 summary: "Replace the import path with a resolvable target",
467 safety: RepairSafety::ScopeLocal,
468};
469
470const REPAIR_IMPORTS_REMOVE_UNUSED: RepairTemplate = RepairTemplate {
471 id: "imports/remove-unused",
472 summary: "Remove the unused import",
473 safety: RepairSafety::BehaviorPreserving,
474};
475
476const REPAIR_IMPORTS_REORDER: RepairTemplate = RepairTemplate {
477 id: "imports/reorder",
478 summary: "Reorder imports into canonical grouping",
479 safety: RepairSafety::FormatOnly,
480};
481
482const REPAIR_ERRORS_CHECK_OR_RESCUE: RepairTemplate = RepairTemplate {
483 id: "errors/check-or-rescue",
484 summary: "Check the result or wrap the call in a `rescue` block",
485 safety: RepairSafety::ScopeLocal,
486};
487
488const REPAIR_ERRORS_WRAP_IN_FN: RepairTemplate = RepairTemplate {
489 id: "errors/wrap-in-fn",
490 summary: "Move the construct inside a function body",
491 safety: RepairSafety::SurfaceChanging,
492};
493
494const REPAIR_MATCH_ADD_MISSING_ARMS: RepairTemplate = RepairTemplate {
495 id: "match/add-missing-arms",
496 summary: "Add arms covering the missing variants",
497 safety: RepairSafety::ScopeLocal,
498};
499
500const REPAIR_MATCH_REMOVE_DUPLICATE_ARM: RepairTemplate = RepairTemplate {
501 id: "match/remove-duplicate-arm",
502 summary: "Remove the duplicated match arm",
503 safety: RepairSafety::BehaviorPreserving,
504};
505
506const REPAIR_FORMAT_REFORMAT: RepairTemplate = RepairTemplate {
507 id: "format/reformat",
508 summary: "Apply canonical formatting",
509 safety: RepairSafety::FormatOnly,
510};
511
512const REPAIR_DOC_COMMENT_MIGRATE: RepairTemplate = RepairTemplate {
513 id: "doc/migrate-comment-style",
514 summary: "Migrate the legacy comment to canonical doc syntax",
515 safety: RepairSafety::FormatOnly,
516};
517
518const REPAIR_DOC_ADD_HARNDOC: RepairTemplate = RepairTemplate {
519 id: "doc/add-harndoc",
520 summary: "Add a `///` doc comment describing this declaration",
521 safety: RepairSafety::BehaviorPreserving,
522};
523
524const REPAIR_DOC_ADD_STDLIB_METADATA: RepairTemplate = RepairTemplate {
525 id: "doc/add-stdlib-metadata",
526 summary: "Add `@effects` and `@errors` fields to the stdlib function's doc block",
527 safety: RepairSafety::BehaviorPreserving,
528};
529
530const REPAIR_BLOCK_REMOVE_EMPTY: RepairTemplate = RepairTemplate {
531 id: "blocks/remove-empty",
532 summary: "Remove the empty block or fill in an explicit body",
533 safety: RepairSafety::ScopeLocal,
534};
535
536const REPAIR_CONTROL_FLOW_FLATTEN: RepairTemplate = RepairTemplate {
537 id: "control-flow/flatten",
538 summary: "Flatten the unnecessary control flow construct",
539 safety: RepairSafety::BehaviorPreserving,
540};
541
542const REPAIR_EXPRESSION_SIMPLIFY: RepairTemplate = RepairTemplate {
543 id: "expressions/simplify",
544 summary: "Simplify the expression to its canonical form",
545 safety: RepairSafety::BehaviorPreserving,
546};
547
548const REPAIR_RECORDS_PICK_FIELDS: RepairTemplate = RepairTemplate {
549 id: "records/pick-fields",
550 summary: "Replace the field-by-field record copy with `pick`",
551 safety: RepairSafety::BehaviorPreserving,
552};
553
554const REPAIR_CLONE_REMOVE_REDUNDANT: RepairTemplate = RepairTemplate {
555 id: "clones/remove-redundant",
556 summary: "Remove the redundant clone",
557 safety: RepairSafety::BehaviorPreserving,
558};
559
560const REPAIR_COLLECTION_PREFER_LAZY: RepairTemplate = RepairTemplate {
561 id: "collections/prefer-lazy",
562 summary: "Replace the eager collection step with a lazy variant",
563 safety: RepairSafety::ScopeLocal,
564};
565
566const REPAIR_DEAD_CODE_REMOVE: RepairTemplate = RepairTemplate {
567 id: "control-flow/remove-dead",
568 summary: "Remove the unreachable code",
569 safety: RepairSafety::BehaviorPreserving,
570};
571
572const REPAIR_STDLIB_MIGRATE_RENAMED: RepairTemplate = RepairTemplate {
573 id: "stdlib/migrate-renamed",
574 summary: "Rename the call to the renamed stdlib symbol",
575 safety: RepairSafety::ScopeLocal,
576};
577
578const REPAIR_BINDINGS_ATTENUATE_HARNESS: RepairTemplate = RepairTemplate {
579 id: "bindings/attenuate-harness",
580 summary: "Replace the root Harness parameter with the single capability the helper uses",
581 safety: RepairSafety::SurfaceChanging,
582};
583
584const REPAIR_LLM_MIGRATE_REMOVED_OPTION: RepairTemplate = RepairTemplate {
585 id: "llm/migrate-removed-option",
586 summary: "Replace the removed option with its supported equivalent",
587 safety: RepairSafety::ScopeLocal,
588};
589
590const REPAIR_LLM_ADD_SCHEMA: RepairTemplate = RepairTemplate {
591 id: "llm/add-schema",
592 summary: "Add a typed output schema to the LLM call",
593 safety: RepairSafety::SurfaceChanging,
594};
595
596const REPAIR_LLM_USE_CAPABILITY_FLAG: RepairTemplate = RepairTemplate {
597 id: "llm/use-capability-flag",
598 summary: "Branch on a capability flag instead of provider identity",
599 safety: RepairSafety::CapabilityChanging,
600};
601
602const REPAIR_PROMPTS_ESCAPE_INJECTION: RepairTemplate = RepairTemplate {
603 id: "prompts/escape-injection",
604 summary: "Pass the untrusted input through a structured placeholder",
605 safety: RepairSafety::ScopeLocal,
606};
607
608const REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE: RepairTemplate = RepairTemplate {
609 id: "prompts/add-tool-to-surface",
610 summary: "Add the referenced tool to the declared tool surface",
611 safety: RepairSafety::SurfaceChanging,
612};
613
614const REPAIR_STYLE_RENAME_TO_CONVENTION: RepairTemplate = RepairTemplate {
615 id: "style/rename-to-convention",
616 summary: "Rename to match the casing convention for this kind",
617 safety: RepairSafety::SurfaceChanging,
618};
619
620const REPAIR_TYPES_ADD_SHAPE_ANNOTATION: RepairTemplate = RepairTemplate {
621 id: "types/add-shape-annotation",
622 summary: "Annotate the dict with a concrete shape type",
623 safety: RepairSafety::SurfaceChanging,
624};
625
626const REPAIR_TYPES_ANNOTATE_PARAMETER: RepairTemplate = RepairTemplate {
636 id: "types/annotate-parameter",
637 summary: "Annotate the parameter with the inferred type, or `unknown` and narrow it at the dynamic boundary",
638 safety: RepairSafety::SurfaceChanging,
639};
640
641const REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE: RepairTemplate = RepairTemplate {
656 id: "types/validate-boundary-value",
657 summary: "Validate the parsed value with schema_expect() or schema_check() before reading it",
658 safety: RepairSafety::ScopeLocal,
659};
660
661const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
662 id: "manual/review-capability-binding",
663 summary: "Review the capability binding; the fix is not mechanical",
664 safety: RepairSafety::NeedsHuman,
665};
666
667const REPAIR_POLICY_NARROW_CHILD_EFFECTS: RepairTemplate = RepairTemplate {
668 id: "policy/narrow-child-effects",
669 summary: "Narrow the child agent's effects to a subset of the parent's, or widen the parent's declared effects",
670 safety: RepairSafety::SurfaceChanging,
671};
672
673const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
674 id: "manual/needs-human",
675 summary: "Plan a human-led change; auto-apply is not safe here",
676 safety: RepairSafety::NeedsHuman,
677};
678
679pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
682 &REPAIR_INSERT_EXPLICIT_CONVERSION,
683 &REPAIR_REWRITE_STRING_INTERPOLATION,
684 &REPAIR_CASTS_REMOVE_UNCHECKED,
685 &REPAIR_CASTS_REMOVE_REDUNDANT,
686 &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
687 &REPAIR_BINDINGS_MAKE_MUTABLE,
688 &REPAIR_BINDINGS_MAKE_IMMUTABLE,
689 &REPAIR_BINDINGS_RENAME_UNUSED,
690 &REPAIR_BINDINGS_REMOVE_UNUSED_PIPELINE_INPUT,
691 &REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER,
692 &REPAIR_BINDINGS_RENAME_SHADOW,
693 &REPAIR_BINDINGS_THREAD_HARNESS,
694 &REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM,
695 &REPAIR_BINDINGS_THREAD_HARNESS_METHOD,
696 &REPAIR_BINDINGS_THREAD_HARNESS_CLOCK,
697 &REPAIR_BINDINGS_THREAD_HARNESS_FS,
698 &REPAIR_BINDINGS_THREAD_HARNESS_ENV,
699 &REPAIR_BINDINGS_THREAD_HARNESS_RANDOM,
700 &REPAIR_BINDINGS_THREAD_HARNESS_NET,
701 &REPAIR_DECLARATIONS_REMOVE_UNUSED,
702 &REPAIR_IMPORTS_FIX_PATH,
703 &REPAIR_IMPORTS_REMOVE_UNUSED,
704 &REPAIR_IMPORTS_REORDER,
705 &REPAIR_ERRORS_CHECK_OR_RESCUE,
706 &REPAIR_ERRORS_WRAP_IN_FN,
707 &REPAIR_MATCH_ADD_MISSING_ARMS,
708 &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
709 &REPAIR_FORMAT_REFORMAT,
710 &REPAIR_DOC_COMMENT_MIGRATE,
711 &REPAIR_DOC_ADD_HARNDOC,
712 &REPAIR_DOC_ADD_STDLIB_METADATA,
713 &REPAIR_BLOCK_REMOVE_EMPTY,
714 &REPAIR_CONTROL_FLOW_FLATTEN,
715 &REPAIR_EXPRESSION_SIMPLIFY,
716 &REPAIR_RECORDS_PICK_FIELDS,
717 &REPAIR_CLONE_REMOVE_REDUNDANT,
718 &REPAIR_COLLECTION_PREFER_LAZY,
719 &REPAIR_DEAD_CODE_REMOVE,
720 &REPAIR_STDLIB_MIGRATE_RENAMED,
721 &REPAIR_BINDINGS_ATTENUATE_HARNESS,
722 &REPAIR_LLM_MIGRATE_REMOVED_OPTION,
723 &REPAIR_LLM_ADD_SCHEMA,
724 &REPAIR_LLM_USE_CAPABILITY_FLAG,
725 &REPAIR_PROMPTS_ESCAPE_INJECTION,
726 &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
727 &REPAIR_STYLE_RENAME_TO_CONVENTION,
728 &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
729 &REPAIR_TYPES_ANNOTATE_PARAMETER,
730 &REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE,
731 &REPAIR_MANUAL_REVIEW_CAPABILITY,
732 &REPAIR_MANUAL_NEEDS_HUMAN,
733 &REPAIR_POLICY_NARROW_CHILD_EFFECTS,
734];
735
736#[cfg(test)]
737mod tests {
738 use super::super::Category;
739 use super::{Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
740 use std::collections::HashSet;
741 use std::str::FromStr;
742
743 #[test]
744 fn parses_registered_code() {
745 assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
746 }
747
748 #[test]
749 fn registry_has_unique_identifiers() {
750 let mut seen = HashSet::new();
751 for entry in Code::registry() {
752 assert!(
753 seen.insert(entry.identifier),
754 "duplicate diagnostic code {}",
755 entry.identifier
756 );
757 assert_eq!(entry.code.as_str(), entry.identifier);
758 assert_eq!(entry.code.category(), entry.category);
759 let expected_prefix = format!("HARN-{}-", entry.category);
760 assert!(entry.identifier.starts_with(&expected_prefix));
761 let suffix = entry.identifier.trim_start_matches(&expected_prefix);
762 assert_eq!(suffix.len(), 3);
763 assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
764 assert!(!entry.summary.is_empty());
765 }
766 assert!(Code::registry().len() >= 40);
767 }
768
769 #[test]
770 fn every_category_is_populated() {
771 for category in Category::ALL {
772 assert!(
773 Code::registry()
774 .iter()
775 .any(|entry| entry.category == *category),
776 "missing diagnostic code category {category}"
777 );
778 }
779 }
780
781 #[test]
782 fn every_code_has_non_empty_explanation() {
783 for entry in Code::registry() {
784 let body = entry.code.explanation();
785 assert!(
786 !body.trim().is_empty(),
787 "diagnostic code {} has an empty explanation file",
788 entry.identifier
789 );
790 assert!(
791 body.contains(entry.identifier),
792 "explanation for {} should reference its identifier",
793 entry.identifier
794 );
795 }
796 }
797
798 #[test]
799 fn related_codes_are_registered_and_non_self() {
800 for entry in Code::registry() {
801 for &other in entry.code.related() {
802 assert_ne!(
803 other, entry.code,
804 "{} lists itself as a related code",
805 entry.identifier
806 );
807 assert!(
808 Code::registry().iter().any(|e| e.code == other),
809 "{} lists unregistered related code {}",
810 entry.identifier,
811 other
812 );
813 }
814 }
815 }
816
817 #[test]
818 fn repair_safety_string_roundtrip() {
819 for safety in RepairSafety::ALL {
820 let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
821 assert_eq!(parsed, *safety);
822 assert_eq!(parsed.to_string(), safety.as_str());
823 }
824 assert_eq!(
825 RepairSafety::from_str("not-a-safety-class"),
826 Err(ParseRepairSafetyError)
827 );
828 }
829
830 #[test]
831 fn repair_safety_ordering_is_monotonic_low_to_high() {
832 let order = RepairSafety::ALL;
836 for window in order.windows(2) {
837 assert!(
838 window[0] < window[1],
839 "{:?} should be safer than {:?}",
840 window[0],
841 window[1]
842 );
843 assert!(window[0].is_at_most(window[1]));
844 assert!(!window[1].is_at_most(window[0]));
845 }
846 }
847
848 #[test]
849 fn only_behavior_preserving_repairs_are_machine_applicable() {
850 assert!(RepairSafety::FormatOnly.is_machine_applicable());
851 assert!(RepairSafety::BehaviorPreserving.is_machine_applicable());
852 for safety in [
853 RepairSafety::ScopeLocal,
854 RepairSafety::SurfaceChanging,
855 RepairSafety::CapabilityChanging,
856 RepairSafety::NeedsHuman,
857 ] {
858 assert!(!safety.is_machine_applicable(), "{safety}");
859 }
860 }
861
862 #[test]
863 fn repair_registry_has_at_least_twenty_entries() {
864 assert!(
865 REPAIR_REGISTRY.len() >= 20,
866 "expected ≥20 repair templates, found {}",
867 REPAIR_REGISTRY.len()
868 );
869 }
870
871 #[test]
872 fn repair_ids_are_kebab_case_namespaced_and_unique() {
873 let mut seen = HashSet::new();
874 for template in REPAIR_REGISTRY {
875 assert!(
876 seen.insert(template.id),
877 "duplicate repair id {}",
878 template.id
879 );
880 let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
881 panic!(
882 "repair id `{}` is missing `<namespace>/` prefix",
883 template.id
884 )
885 });
886 assert!(
887 !namespace.is_empty() && !leaf.is_empty(),
888 "repair id `{}` has empty namespace or leaf",
889 template.id
890 );
891 for ch in template.id.chars() {
892 assert!(
893 ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
894 "repair id `{}` has non-kebab character {ch:?}",
895 template.id
896 );
897 }
898 assert!(
899 !template.summary.is_empty(),
900 "repair {} has empty summary",
901 template.id
902 );
903 let first = template.summary.chars().next().unwrap();
905 assert!(
906 first.is_ascii_uppercase(),
907 "repair {} summary `{}` should start with a capital",
908 template.id,
909 template.summary
910 );
911 }
912 }
913
914 #[test]
915 fn manual_namespace_is_needs_human() {
916 for template in REPAIR_REGISTRY {
917 if let Some(("manual", _)) = template.id.split_once('/') {
918 assert_eq!(
919 template.safety,
920 RepairSafety::NeedsHuman,
921 "manual/* repair {} must be NeedsHuman",
922 template.id
923 );
924 }
925 }
926 }
927
928 #[test]
929 fn known_codes_carry_expected_safety_class() {
930 let expected: &[(Code, RepairSafety, &str)] = &[
934 (
935 Code::FormatterWouldReformat,
936 RepairSafety::FormatOnly,
937 "format/reformat",
938 ),
939 (
940 Code::ModuleImportUnused,
941 RepairSafety::BehaviorPreserving,
942 "imports/remove-unused",
943 ),
944 (
945 Code::ImmutableAssignment,
946 RepairSafety::ScopeLocal,
947 "bindings/make-mutable",
948 ),
949 (
950 Code::LintUnusedFunction,
951 RepairSafety::SurfaceChanging,
952 "declarations/remove-unused",
953 ),
954 (
955 Code::LlmProviderIdentityBranch,
956 RepairSafety::CapabilityChanging,
957 "llm/use-capability-flag",
958 ),
959 (
960 Code::PromptVariantExplosion,
961 RepairSafety::NeedsHuman,
962 "manual/needs-human",
963 ),
964 (
965 Code::NonExhaustiveMatch,
966 RepairSafety::ScopeLocal,
967 "match/add-missing-arms",
968 ),
969 (
970 Code::LintAmbientClockBuiltin,
971 RepairSafety::ScopeLocal,
972 "bindings/thread-harness-clock",
973 ),
974 (
975 Code::LintAmbientStdioBuiltin,
976 RepairSafety::ScopeLocal,
977 "bindings/thread-harness",
978 ),
979 (
980 Code::InvalidMainSignature,
981 RepairSafety::SurfaceChanging,
982 "bindings/thread-harness-needs-param",
983 ),
984 ];
985 for (code, safety, repair_id) in expected {
986 let template = code
987 .repair_template()
988 .unwrap_or_else(|| panic!("{code} should have a repair template"));
989 assert_eq!(template.safety, *safety, "{code} safety class drifted");
990 assert_eq!(template.id, *repair_id, "{code} repair id drifted");
991 }
992 }
993
994 #[test]
995 fn repair_templates_cover_at_least_twenty_codes() {
996 let covered = Code::ALL
997 .iter()
998 .filter(|code| code.repair_template().is_some())
999 .count();
1000 assert!(
1001 covered >= 20,
1002 "expected ≥20 codes with a repair template, found {covered}"
1003 );
1004 }
1005
1006 #[test]
1020 fn boundary_validation_codes_offer_the_most_informative_repair() {
1021 for code in [Code::BoundaryValueUnvalidated, Code::LintUntypedDictAccess] {
1022 let Some(template) = code.repair_template() else {
1023 continue;
1024 };
1025 assert_ne!(
1026 template.id, "types/add-shape-annotation",
1027 "{code:?} is a boundary-validation rule; an annotation validates the value \
1028 (harn#6252) but reports only the declared type and the value's kind, so the \
1029 offered one-step repair must be the one that names the failing field"
1030 );
1031 }
1032 }
1033
1034 #[test]
1035 fn every_registered_repair_is_referenced_by_some_code() {
1036 let referenced: HashSet<&'static str> = Code::ALL
1037 .iter()
1038 .filter_map(|code| code.repair_template())
1039 .map(|template| template.id)
1040 .collect();
1041 for template in REPAIR_REGISTRY {
1042 assert!(
1043 referenced.contains(template.id),
1044 "repair {} is in REPAIR_REGISTRY but no Code maps to it",
1045 template.id
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn every_referenced_repair_template_is_in_registry() {
1052 let registered: HashSet<&'static str> =
1053 REPAIR_REGISTRY.iter().map(|template| template.id).collect();
1054 for code in Code::ALL {
1055 let Some(template) = code.repair_template() else {
1056 continue;
1057 };
1058 assert!(
1059 registered.contains(template.id),
1060 "repair {} (used by {}) is missing from REPAIR_REGISTRY",
1061 template.id,
1062 code
1063 );
1064 }
1065 }
1066}