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
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#[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#[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#[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#[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 pub const fn repair_template(self) -> Option<&'static RepairTemplate> {
182 match self {
183 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 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 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 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 Code::DeprecatedStdlibSymbol => Some(&REPAIR_STDLIB_MIGRATE_RENAMED),
234 Code::LintMissingStdlibMetadata => Some(&REPAIR_DOC_ADD_STDLIB_METADATA),
235
236 Code::ImmutableAssignment => Some(&REPAIR_BINDINGS_MAKE_MUTABLE),
238 Code::MutableNeverReassigned => Some(&REPAIR_BINDINGS_MAKE_IMMUTABLE),
239
240 Code::NonExhaustiveMatch => Some(&REPAIR_MATCH_ADD_MISSING_ARMS),
242 Code::DuplicateMatchArm => Some(&REPAIR_MATCH_REMOVE_DUPLICATE_ARM),
243
244 Code::UnreachableCode => Some(&REPAIR_DEAD_CODE_REMOVE),
246
247 Code::FormatterWouldReformat | Code::FormatterTrailingComma => {
249 Some(&REPAIR_FORMAT_REFORMAT)
250 }
251
252 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 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 _ => None,
318 }
319 }
320}
321
322const 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
606const REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE: RepairTemplate = RepairTemplate {
614 id: "types/validate-boundary-value",
615 summary: "Validate the parsed value with schema_expect() or schema_check() before reading it",
616 safety: RepairSafety::ScopeLocal,
617};
618
619const REPAIR_MANUAL_REVIEW_CAPABILITY: RepairTemplate = RepairTemplate {
620 id: "manual/review-capability-binding",
621 summary: "Review the capability binding; the fix is not mechanical",
622 safety: RepairSafety::NeedsHuman,
623};
624
625const REPAIR_POLICY_NARROW_CHILD_EFFECTS: RepairTemplate = RepairTemplate {
626 id: "policy/narrow-child-effects",
627 summary: "Narrow the child agent's effects to a subset of the parent's, or widen the parent's declared effects",
628 safety: RepairSafety::SurfaceChanging,
629};
630
631const REPAIR_MANUAL_NEEDS_HUMAN: RepairTemplate = RepairTemplate {
632 id: "manual/needs-human",
633 summary: "Plan a human-led change; auto-apply is not safe here",
634 safety: RepairSafety::NeedsHuman,
635};
636
637pub const REPAIR_REGISTRY: &[&RepairTemplate] = &[
640 &REPAIR_INSERT_EXPLICIT_CONVERSION,
641 &REPAIR_REWRITE_STRING_INTERPOLATION,
642 &REPAIR_CASTS_REMOVE_UNCHECKED,
643 &REPAIR_CASTS_REMOVE_REDUNDANT,
644 &REPAIR_BINDINGS_RENAME_TO_CLOSEST,
645 &REPAIR_BINDINGS_MAKE_MUTABLE,
646 &REPAIR_BINDINGS_MAKE_IMMUTABLE,
647 &REPAIR_BINDINGS_RENAME_UNUSED,
648 &REPAIR_BINDINGS_NAME_CAPABILITY_PARAMETER,
649 &REPAIR_BINDINGS_RENAME_SHADOW,
650 &REPAIR_BINDINGS_THREAD_HARNESS,
651 &REPAIR_BINDINGS_THREAD_HARNESS_NEEDS_PARAM,
652 &REPAIR_BINDINGS_THREAD_HARNESS_METHOD,
653 &REPAIR_BINDINGS_THREAD_HARNESS_CLOCK,
654 &REPAIR_BINDINGS_THREAD_HARNESS_FS,
655 &REPAIR_BINDINGS_THREAD_HARNESS_ENV,
656 &REPAIR_BINDINGS_THREAD_HARNESS_RANDOM,
657 &REPAIR_BINDINGS_THREAD_HARNESS_NET,
658 &REPAIR_DECLARATIONS_REMOVE_UNUSED,
659 &REPAIR_IMPORTS_FIX_PATH,
660 &REPAIR_IMPORTS_REMOVE_UNUSED,
661 &REPAIR_IMPORTS_REORDER,
662 &REPAIR_ERRORS_CHECK_OR_RESCUE,
663 &REPAIR_ERRORS_WRAP_IN_FN,
664 &REPAIR_MATCH_ADD_MISSING_ARMS,
665 &REPAIR_MATCH_REMOVE_DUPLICATE_ARM,
666 &REPAIR_FORMAT_REFORMAT,
667 &REPAIR_DOC_COMMENT_MIGRATE,
668 &REPAIR_DOC_ADD_HARNDOC,
669 &REPAIR_DOC_ADD_STDLIB_METADATA,
670 &REPAIR_BLOCK_REMOVE_EMPTY,
671 &REPAIR_CONTROL_FLOW_FLATTEN,
672 &REPAIR_EXPRESSION_SIMPLIFY,
673 &REPAIR_CLONE_REMOVE_REDUNDANT,
674 &REPAIR_COLLECTION_PREFER_LAZY,
675 &REPAIR_DEAD_CODE_REMOVE,
676 &REPAIR_STDLIB_MIGRATE_RENAMED,
677 &REPAIR_BINDINGS_ATTENUATE_HARNESS,
678 &REPAIR_LLM_MIGRATE_DEPRECATED_OPTION,
679 &REPAIR_LLM_ADD_SCHEMA,
680 &REPAIR_LLM_USE_CAPABILITY_FLAG,
681 &REPAIR_PROMPTS_ESCAPE_INJECTION,
682 &REPAIR_PROMPTS_ADD_TOOL_TO_SURFACE,
683 &REPAIR_STYLE_RENAME_TO_CONVENTION,
684 &REPAIR_TYPES_ADD_SHAPE_ANNOTATION,
685 &REPAIR_TYPES_VALIDATE_BOUNDARY_VALUE,
686 &REPAIR_MANUAL_REVIEW_CAPABILITY,
687 &REPAIR_MANUAL_NEEDS_HUMAN,
688 &REPAIR_POLICY_NARROW_CHILD_EFFECTS,
689];
690
691#[cfg(test)]
692mod tests {
693 use super::super::Category;
694 use super::{Code, ParseRepairSafetyError, RepairSafety, REPAIR_REGISTRY};
695 use std::collections::HashSet;
696 use std::str::FromStr;
697
698 #[test]
699 fn parses_registered_code() {
700 assert_eq!(Code::from_str("HARN-TYP-014"), Ok(Code::TypeParameterArity));
701 }
702
703 #[test]
704 fn registry_has_unique_identifiers() {
705 let mut seen = HashSet::new();
706 for entry in Code::registry() {
707 assert!(
708 seen.insert(entry.identifier),
709 "duplicate diagnostic code {}",
710 entry.identifier
711 );
712 assert_eq!(entry.code.as_str(), entry.identifier);
713 assert_eq!(entry.code.category(), entry.category);
714 let expected_prefix = format!("HARN-{}-", entry.category);
715 assert!(entry.identifier.starts_with(&expected_prefix));
716 let suffix = entry.identifier.trim_start_matches(&expected_prefix);
717 assert_eq!(suffix.len(), 3);
718 assert!(suffix.chars().all(|ch| ch.is_ascii_digit()));
719 assert!(!entry.summary.is_empty());
720 }
721 assert!(Code::registry().len() >= 40);
722 }
723
724 #[test]
725 fn every_category_is_populated() {
726 for category in Category::ALL {
727 assert!(
728 Code::registry()
729 .iter()
730 .any(|entry| entry.category == *category),
731 "missing diagnostic code category {category}"
732 );
733 }
734 }
735
736 #[test]
737 fn every_code_has_non_empty_explanation() {
738 for entry in Code::registry() {
739 let body = entry.code.explanation();
740 assert!(
741 !body.trim().is_empty(),
742 "diagnostic code {} has an empty explanation file",
743 entry.identifier
744 );
745 assert!(
746 body.contains(entry.identifier),
747 "explanation for {} should reference its identifier",
748 entry.identifier
749 );
750 }
751 }
752
753 #[test]
754 fn related_codes_are_registered_and_non_self() {
755 for entry in Code::registry() {
756 for &other in entry.code.related() {
757 assert_ne!(
758 other, entry.code,
759 "{} lists itself as a related code",
760 entry.identifier
761 );
762 assert!(
763 Code::registry().iter().any(|e| e.code == other),
764 "{} lists unregistered related code {}",
765 entry.identifier,
766 other
767 );
768 }
769 }
770 }
771
772 #[test]
773 fn repair_safety_string_roundtrip() {
774 for safety in RepairSafety::ALL {
775 let parsed = RepairSafety::from_str(safety.as_str()).unwrap();
776 assert_eq!(parsed, *safety);
777 assert_eq!(parsed.to_string(), safety.as_str());
778 }
779 assert_eq!(
780 RepairSafety::from_str("not-a-safety-class"),
781 Err(ParseRepairSafetyError)
782 );
783 }
784
785 #[test]
786 fn repair_safety_ordering_is_monotonic_low_to_high() {
787 let order = RepairSafety::ALL;
791 for window in order.windows(2) {
792 assert!(
793 window[0] < window[1],
794 "{:?} should be safer than {:?}",
795 window[0],
796 window[1]
797 );
798 assert!(window[0].is_at_most(window[1]));
799 assert!(!window[1].is_at_most(window[0]));
800 }
801 }
802
803 #[test]
804 fn repair_registry_has_at_least_twenty_entries() {
805 assert!(
806 REPAIR_REGISTRY.len() >= 20,
807 "expected ≥20 repair templates, found {}",
808 REPAIR_REGISTRY.len()
809 );
810 }
811
812 #[test]
813 fn repair_ids_are_kebab_case_namespaced_and_unique() {
814 let mut seen = HashSet::new();
815 for template in REPAIR_REGISTRY {
816 assert!(
817 seen.insert(template.id),
818 "duplicate repair id {}",
819 template.id
820 );
821 let (namespace, leaf) = template.id.split_once('/').unwrap_or_else(|| {
822 panic!(
823 "repair id `{}` is missing `<namespace>/` prefix",
824 template.id
825 )
826 });
827 assert!(
828 !namespace.is_empty() && !leaf.is_empty(),
829 "repair id `{}` has empty namespace or leaf",
830 template.id
831 );
832 for ch in template.id.chars() {
833 assert!(
834 ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '/',
835 "repair id `{}` has non-kebab character {ch:?}",
836 template.id
837 );
838 }
839 assert!(
840 !template.summary.is_empty(),
841 "repair {} has empty summary",
842 template.id
843 );
844 let first = template.summary.chars().next().unwrap();
846 assert!(
847 first.is_ascii_uppercase(),
848 "repair {} summary `{}` should start with a capital",
849 template.id,
850 template.summary
851 );
852 }
853 }
854
855 #[test]
856 fn manual_namespace_is_needs_human() {
857 for template in REPAIR_REGISTRY {
858 if let Some(("manual", _)) = template.id.split_once('/') {
859 assert_eq!(
860 template.safety,
861 RepairSafety::NeedsHuman,
862 "manual/* repair {} must be NeedsHuman",
863 template.id
864 );
865 }
866 }
867 }
868
869 #[test]
870 fn known_codes_carry_expected_safety_class() {
871 let expected: &[(Code, RepairSafety, &str)] = &[
875 (
876 Code::FormatterWouldReformat,
877 RepairSafety::FormatOnly,
878 "format/reformat",
879 ),
880 (
881 Code::ModuleImportUnused,
882 RepairSafety::BehaviorPreserving,
883 "imports/remove-unused",
884 ),
885 (
886 Code::ImmutableAssignment,
887 RepairSafety::ScopeLocal,
888 "bindings/make-mutable",
889 ),
890 (
891 Code::LintUnusedFunction,
892 RepairSafety::SurfaceChanging,
893 "declarations/remove-unused",
894 ),
895 (
896 Code::LlmProviderIdentityBranch,
897 RepairSafety::CapabilityChanging,
898 "llm/use-capability-flag",
899 ),
900 (
901 Code::PromptVariantExplosion,
902 RepairSafety::NeedsHuman,
903 "manual/needs-human",
904 ),
905 (
906 Code::NonExhaustiveMatch,
907 RepairSafety::ScopeLocal,
908 "match/add-missing-arms",
909 ),
910 (
911 Code::LintAmbientClockBuiltin,
912 RepairSafety::ScopeLocal,
913 "bindings/thread-harness-clock",
914 ),
915 (
916 Code::LintAmbientStdioBuiltin,
917 RepairSafety::ScopeLocal,
918 "bindings/thread-harness",
919 ),
920 (
921 Code::InvalidMainSignature,
922 RepairSafety::SurfaceChanging,
923 "bindings/thread-harness-needs-param",
924 ),
925 ];
926 for (code, safety, repair_id) in expected {
927 let template = code
928 .repair_template()
929 .unwrap_or_else(|| panic!("{code} should have a repair template"));
930 assert_eq!(template.safety, *safety, "{code} safety class drifted");
931 assert_eq!(template.id, *repair_id, "{code} repair id drifted");
932 }
933 }
934
935 #[test]
936 fn repair_templates_cover_at_least_twenty_codes() {
937 let covered = Code::ALL
938 .iter()
939 .filter(|code| code.repair_template().is_some())
940 .count();
941 assert!(
942 covered >= 20,
943 "expected ≥20 codes with a repair template, found {covered}"
944 );
945 }
946
947 #[test]
961 fn boundary_validation_codes_are_not_repaired_by_an_annotation() {
962 for code in [Code::BoundaryValueUnvalidated, Code::LintUntypedDictAccess] {
963 let Some(template) = code.repair_template() else {
964 continue;
965 };
966 assert_ne!(
967 template.id, "types/add-shape-annotation",
968 "{code:?} is a boundary-validation rule; its repair must validate, not annotate"
969 );
970 }
971 }
972
973 #[test]
974 fn every_registered_repair_is_referenced_by_some_code() {
975 let referenced: HashSet<&'static str> = Code::ALL
976 .iter()
977 .filter_map(|code| code.repair_template())
978 .map(|template| template.id)
979 .collect();
980 for template in REPAIR_REGISTRY {
981 assert!(
982 referenced.contains(template.id),
983 "repair {} is in REPAIR_REGISTRY but no Code maps to it",
984 template.id
985 );
986 }
987 }
988
989 #[test]
990 fn every_referenced_repair_template_is_in_registry() {
991 let registered: HashSet<&'static str> =
992 REPAIR_REGISTRY.iter().map(|template| template.id).collect();
993 for code in Code::ALL {
994 let Some(template) = code.repair_template() else {
995 continue;
996 };
997 assert!(
998 registered.contains(template.id),
999 "repair {} (used by {}) is missing from REPAIR_REGISTRY",
1000 template.id,
1001 code
1002 );
1003 }
1004 }
1005}