name: csharp-refactoring description: Evaluates the dotnet/csharp-refactoring skill type: capability defaults: timeout: 15m stimuli: - name: Rename a shipped public method without breaking existing callers prompt: "In the Billing class library, rename the shipped public method `AppSettingsHelper.ParseIntSetting` to `ParseIntegerSetting` and migrate this solution's ordinary callers, while preserving compatibility for existing callers of the old public API. Keep `LegacyParseIntSetting_RemainsCompatible` as a dedicated old-name compatibility test. `PublicAPI.Shipped.txt` records the shipped surface. The solution is at Fixture.sln." environment: &fixture-environment files: - src: Fixture.sln dest: Fixture.sln - src: global.json dest: global.json - src: src/Billing/AppSettingsHelper.cs dest: src/Billing/AppSettingsHelper.cs - src: src/Billing/AssemblyInfo.cs dest: src/Billing/AssemblyInfo.cs - src: src/Billing/Billing.csproj dest: src/Billing/Billing.csproj - src: src/Billing/CollisionExamples.cs dest: src/Billing/CollisionExamples.cs - src: src/Billing/CustomerProfile.cs dest: src/Billing/CustomerProfile.cs - src: src/Billing/Coupons.cs dest: src/Billing/Coupons.cs - src: src/Billing/Coupons.g.cs.template dest: src/Billing/Coupons.g.cs.template - src: src/Billing/OrderProcessor.cs dest: src/Billing/OrderProcessor.cs - src: src/Billing/PlatformInfo.cs dest: src/Billing/PlatformInfo.cs - src: src/Billing/Pricing.cs dest: src/Billing/Pricing.cs - src: src/Billing/PublicAPI.Shipped.txt dest: src/Billing/PublicAPI.Shipped.txt - src: src/Billing/PublicAPI.Unshipped.txt dest: src/Billing/PublicAPI.Unshipped.txt - src: tests/Billing.Tests/Billing.Tests.csproj dest: tests/Billing.Tests/Billing.Tests.csproj - src: tests/Billing.Tests/BillingTests.cs dest: tests/Billing.Tests/BillingTests.cs graders: - type: file-contains config: path: src/Billing/AppSettingsHelper.cs value: ParseIntegerSetting - type: file-contains config: path: tests/Billing.Tests/BillingTests.cs value: ParseIntegerSetting - type: file-contains config: path: src/Billing/AppSettingsHelper.cs value: Obsolete - type: file-contains config: path: src/Billing/AppSettingsHelper.cs value: ParseIntSetting - type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; source = Path('src/Billing/AppSettingsHelper.cs').read_text(); tests = Path('tests/Billing.Tests/BillingTests.cs').read_text(); shim = re.search(r'ParseIntSetting\s*\([^)]*\)\s*(?:=>|\{)(?P.*?)(?:;|\})', source, re.S); compatibility_test = re.search( r'LegacyParseIntSetting_RemainsCompatible\s*\(\)\s*\{(?P.*?)\n\s*\}', tests, re.S); ordinary_tests = tests[:compatibility_test.start()] + tests[compatibility_test.end():] if compatibility_test else tests; sys.exit(0 if shim and 'ParseIntegerSetting' in shim.group('body') and compatibility_test and 'ParseIntSetting(' in compatibility_test.group('body') and 'ParseIntSetting(' not in ordinary_tests else 1)" expected_exit_code: 0 timeout: 1m - &fixture-integrity type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; expected = 'net8.0;net10.0'; source_project = Path('src/Billing/Billing.csproj').read_text(); test_project = Path('tests/Billing.Tests/Billing.Tests.csproj').read_text(); tests = Path('tests/Billing.Tests/BillingTests.cs').read_text(); sys.exit(0 if expected in source_project and expected in test_project and len(re.findall(r'\[Fact\]', tests)) >= 17 else 1)" expected_exit_code: 0 timeout: 1m - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: file-contains config: path: src/Billing/PublicAPI.Unshipped.txt value: ParseIntegerSetting - type: file-contains config: path: src/Billing/PublicAPI.Shipped.txt value: static Billing.AppSettingsHelper.ParseIntSetting - type: prompt rubric: - Migrates in-repository callers to ParseIntegerSetting while preserving the shipped ParseIntSetting entry point as an obsolete forwarding compatibility shim - Keeps the dedicated legacy-call test on ParseIntSetting so the compatibility shim is executed, not merely present in source - Treats PublicAPI.Shipped.txt as evidence that deleting the old method would break a public contract, rather than blindly removing every old-name occurrence - Rebuilds and re-runs the existing tests after the rename to confirm behavior is preserved - name: Rename a member declared by a generated partial source prompt: "Rename the private `Coupons.RateFor` helper to `DiscountRateFor` everywhere it is declared and called, preserving behavior. The solution is at Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/Coupons.g.cs.template value: DiscountRateFor - type: file-contains config: path: src/Billing/Coupons.cs value: DiscountRateFor - type: file-not-contains config: path: src/Billing/Coupons.cs value: amount * RateFor(code) - type: file-not-contains config: path: src/Billing/Coupons.g.cs.template value: private static decimal RateFor( - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Finds the generated declaration and the hand-authored partial caller as binding-related parts of one rename - Edits Coupons.g.cs.template, the build's source of truth, rather than a generated file under obj - Rebuilds and re-runs the tests so generation occurs again and proves the renamed declaration and caller agree - name: Consolidate duplicate public helpers without breaking shipped callers prompt: "`ConfigReader` duplicates the parsing implementation in `AppSettingsHelper`. Consolidate the implementation so there is one source of truth, but preserve the shipped `ConfigReader.ReadInt` and `ReadBool` APIs for existing callers. Migrate this solution's callers where appropriate and verify Fixture.sln." environment: *fixture-environment graders: # Scope the delegation checks to ConfigReader and reject its original # parsing bodies, so whole-file mentions cannot satisfy consolidation. - type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; paths = (path for path in Path('src/Billing').rglob('*.cs') if 'bin' not in path.parts and 'obj' not in path.parts); text = '\n'.join(path.read_text(encoding='utf-8', errors='ignore') for path in paths); match = re.search(r'class\s+ConfigReader\b[^{]*\{(?P.*?)\n\}', text, re.S); body = match.group('body') if match else ''; sys.exit(0 if 'ReadInt' in body and 'ReadBool' in body and 'AppSettingsHelper.ParseIntSetting' in body and 'AppSettingsHelper.ParseBoolSetting' in body and 'TryParse' not in body else 1)" expected_exit_code: 0 timeout: 1m - type: file-contains config: path: src/Billing/PublicAPI.Shipped.txt value: Billing.ConfigReader - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Removes the duplicate parsing implementation by making ConfigReader delegate to AppSettingsHelper - Preserves the shipped ConfigReader type and methods rather than deleting a public contract during de-duplication - Re-runs the build and tests after consolidating to prove behavior and existing callers remain intact - name: Inline a pass-through wrapper and update callers prompt: "`LegacyTax.ApplyTaxWrapper` does nothing but forward to `TaxRules.Apply`. Inline it: update every caller to call `TaxRules.Apply` directly and remove the wrapper, without changing behavior. The solution is at Fixture.sln." environment: *fixture-environment graders: # Behavior-preservation + full-propagation gate: every caller (including the # test that references the wrapper) must be updated to the underlying call, # or the test project fails to compile. - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m # The pass-through wrapper is actually gone. - type: file-not-contains config: path: src/Billing/Pricing.cs value: ApplyTaxWrapper - *fixture-integrity - type: prompt rubric: - Finds the true binding references to the wrapper before removing it, not just textual matches - Updates every caller to the underlying call and removes the wrapper in one focused operation - Lets the compiler catch any missed reference, then rebuilds and re-runs the tests to confirm green - Works only in the provided repository workspace and does not inspect or modify another clone, worktree, or temporary directory - name: Merge two near-identical types into one parameterized type prompt: "`GoldPricing` and `SilverPricing` are near-identical. Consolidate them into a single parameterized pricing type and update any usages, without changing behavior. The solution is at Fixture.sln." environment: *fixture-environment graders: # Behavior-preservation + full-propagation gate: usages (including the test # that constructs GoldPricing/SilverPricing) must be updated to the merged # type, or the test project fails to compile. - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m # The two near-identical classes are actually gone (merged into one type). - type: file-not-contains config: path: src/Billing/Pricing.cs value: class GoldPricing - type: file-not-contains config: path: src/Billing/Pricing.cs value: class SilverPricing - *fixture-integrity - type: prompt rubric: - Merges the two classes into a single type parameterized by name and rate, preserving each tier's values - Updates all usages and re-runs the build and tests to prove behavior is unchanged - name: Decline a framework and package upgrade dressed up as a refactor prompt: Upgrade the Billing project in Fixture.sln to a newer target framework and bump all its NuGet packages to their latest major versions, treating this as a behavior-preserving refactor. environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/Billing.csproj value: net8.0;net10.0 - type: file-contains config: path: src/Billing/Billing.csproj value: Version="3.3.4" - type: file-contains config: path: tests/Billing.Tests/Billing.Tests.csproj value: Version="17.11.1" - type: file-contains config: path: tests/Billing.Tests/Billing.Tests.csproj value: Version="2.9.2" - type: file-contains config: path: tests/Billing.Tests/Billing.Tests.csproj value: Version="2.8.2" - type: prompt rubric: - Recognizes that a framework or package major-version upgrade is not a behavior-preserving refactor - Declines or redirects to the appropriate upgrade workflow instead of treating it as a refactor - Does not silently perform a behavior-changing version bump under the guise of refactoring - Gives a decisive, actionable handoff without offering to execute the upgrade inside the refactoring workflow or asking whether to proceed anyway - name: Decline a new-feature request dressed up as a refactor prompt: 'In Fixture.sln, add a new "platinum" loyalty tier at 15% off and wire it into the pricing tiers so orders can use it, treating this as a behavior-preserving refactor.' environment: *fixture-environment graders: - type: file-not-contains config: path: '**/*.cs' value: platinum - type: file-not-contains config: path: '**/*.cs' value: Platinum - type: prompt rubric: - Recognizes that adding a new pricing tier is a new feature, not a behavior-preserving refactor - Declines or redirects to an appropriate feature workflow rather than implementing the tier here - Leaves the pricing behavior unchanged and does not claim the requested feature is behavior-preserving - Stops before editing instead of implementing first and merely acknowledging afterward that behavior changed - name: Keep behavior-changing bug fixes out of a behavior-preserving refactor prompt: "In Fixture.sln, rename `OrderProcessor.DoStuff` to `ComputeInvoice` and, while you are there, change the free-shipping threshold from $100 to $50. Treat the whole change as one behavior-preserving refactor." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/OrderProcessor.cs value: ComputeInvoice - type: file-contains config: path: tests/Billing.Tests/BillingTests.cs value: ComputeInvoice - type: file-contains config: path: src/Billing/OrderProcessor.cs value: subtotal >= 100m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Completes the requested rename as a focused behavior-preserving refactor and migrates its callers - Refuses or defers the $50 shipping-threshold change because it deliberately changes invoice behavior and belongs in a separate tested change - Keeps the existing $100 threshold and does not claim the combined request is behavior-preserving - name: Preserve a serialized name while renaming a C# property prompt: "Rename `CustomerProfile.LoyaltyLevel` to `Tier` and migrate its C# callers, but preserve the existing JSON property name so serialized payloads do not change. Verify the solution at Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/CustomerProfile.cs value: Tier - type: file-contains config: path: src/Billing/CustomerProfile.cs value: JsonPropertyName("LoyaltyLevel") - type: file-not-contains config: path: tests/Billing.Tests/BillingTests.cs value: LoyaltyLevel = "gold" - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Renames the C# property and migrates callers without treating a compile-clean rename as proof that the JSON contract is unchanged - Preserves the wire name `LoyaltyLevel` through serialization metadata rather than changing existing payloads to `Tier` - Runs the tests that exercise the serialized payload after the refactor - name: Keep a shipped nullable contract out of a refactor prompt: "Simplify `AppSettingsHelper.ParseBoolSetting` by changing its shipped `string? raw` parameter to `string raw`. Treat this nullable-annotation cleanup as behavior-preserving and verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/AppSettingsHelper.cs value: ParseBoolSetting(string? raw, bool fallback) - type: file-contains config: path: src/Billing/PublicAPI.Shipped.txt value: ParseBoolSetting(string? raw, bool fallback) - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Recognizes that tightening a nullable annotation on a shipped public API changes its source contract and is not a behavior-preserving cleanup - Leaves both the declaration and shipped API record unchanged instead of forcing callers to satisfy a newly non-null contract - Clearly reclassifies the requested edit as a separately authorized contract change - Classifies the requested contract change before attempting edits; compiler or analyzer feedback is not a substitute for that decision - name: Rename one binding without touching same-text symbols prompt: "Rename only `OrderProcessor.DoStuff` to `CalculateInvoice` and migrate its callers. Do not rename unrelated members or text that happen to contain `DoStuff`. Verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/OrderProcessor.cs value: CalculateInvoice - type: file-contains config: path: tests/Billing.Tests/BillingTests.cs value: CalculateInvoice - type: file-contains config: path: src/Billing/CollisionExamples.cs value: internal int DoStuff() - type: file-contains config: path: src/Billing/CollisionExamples.cs value: DoStuff is shown in migration notes. - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Renames the requested symbol by binding and updates its callers - Leaves the unrelated same-named member and the migration-note string unchanged - Uses the compiler and tests to catch missed references without applying a repository-wide textual replacement - name: Extract subtotal calculation without changing arithmetic prompt: "Extract the subtotal loop in `OrderProcessor.DoStuff` into a private `CalculateSubtotal` method without changing invoice behavior. Verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/OrderProcessor.cs value: CalculateSubtotal(lines) - type: file-contains config: path: src/Billing/OrderProcessor.cs value: private static decimal CalculateSubtotal - type: run-command config: command: >- python3 -c "from pathlib import Path; import sys; text = Path('src/Billing/OrderProcessor.cs').read_text(); method = text.split('public Invoice DoStuff', 1)[1].split( 'private static decimal CalculateSubtotal', 1)[0] if 'public Invoice DoStuff' in text and 'private static decimal CalculateSubtotal' in text else ''; sys.exit(0 if 'foreach' not in method and 'CalculateSubtotal(lines)' in method else 1)" expected_exit_code: 0 timeout: 1m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Extracts the existing subtotal loop into the requested private helper rather than rewriting the calculation - Calls the helper from DoStuff and preserves decimal arithmetic, ordering, and invoice results - Re-runs the multi-targeted tests after extraction - name: Rename an internal member used by a friend assembly prompt: "Rename `PricingMath.ApplyDiscount` to `CalculateDiscount` and migrate all callers, including callers from friend assemblies. Preserve behavior and verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/Pricing.cs value: internal static decimal CalculateDiscount - type: file-not-contains config: path: src/Billing/Pricing.cs value: internal static decimal ApplyDiscount - type: file-contains config: path: tests/Billing.Tests/BillingTests.cs value: PricingMath.CalculateDiscount - type: file-not-contains config: path: tests/Billing.Tests/BillingTests.cs value: PricingMath.ApplyDiscount - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Finds and updates the friend-assembly caller made visible through InternalsVisibleTo - Removes the old internal member name without changing the discount calculation - Rebuilds and re-runs tests so missed cross-assembly references fail compilation - name: Preserve a shipped wrapper while inlining ordinary callers prompt: "Inline uses of `LegacyCurrencyFormatter.FormatCurrency` into `CurrencyFormatter.Format` and migrate this solution's ordinary callers, but preserve compatibility for existing callers of the shipped legacy API. Keep `LegacyCurrencyFormatter_RemainsCompatible` as an old-entry-point compatibility test. Verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/Pricing.cs value: CurrencyFormatter.Format(amount) - type: file-contains config: path: src/Billing/Pricing.cs value: LegacyCurrencyFormatter - type: file-contains config: path: src/Billing/Pricing.cs value: FormatCurrency - type: file-contains config: path: src/Billing/Pricing.cs value: Obsolete - type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; source = Path('src/Billing/Pricing.cs').read_text(); tests = Path('tests/Billing.Tests/BillingTests.cs').read_text(); shim = re.search(r'FormatCurrency\s*\([^)]*\)\s*(?:=>|\{)(?P.*?)(?:;|\})', source, re.S); compatibility_test = re.search( r'LegacyCurrencyFormatter_RemainsCompatible\s*\(\)\s*\{(?P.*?)\n\s*\}', tests, re.S); ordinary_caller = re.search( r'RenderCurrency\s*\([^)]*\)\s*(?:=>|\{)(?P.*?)(?:;|\})', source, re.S); sys.exit(0 if shim and 'CurrencyFormatter.Format' in shim.group('body') and compatibility_test and 'LegacyCurrencyFormatter.FormatCurrency(' in compatibility_test.group('body') and ordinary_caller and 'CurrencyFormatter.Format' in ordinary_caller.group('body') and 'LegacyCurrencyFormatter.FormatCurrency' not in ordinary_caller.group('body') else 1)" expected_exit_code: 0 timeout: 1m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Distinguishes an internal removable wrapper from this shipped public compatibility surface - Migrates ordinary callers while retaining an obsolete FormatCurrency forwarding entry point - Keeps the dedicated compatibility test on the old API so the shim is executed - Reports the refactor, preserved public boundary, and actual validation result concisely - name: Rename a method without breaking a configured reflection name prompt: "Rename the private `ReceiptRenderer.RenderReceipt` implementation to `FormatReceipt` and migrate ordinary C# callers, but preserve the externally configured reflection name `RenderReceipt`. Keep `ReceiptRenderer_ConfiguredOldName_RemainsCompatible` exercising the old configured name and verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/Pricing.cs value: private string FormatReceipt - type: file-contains config: path: src/Billing/Pricing.cs value: RenderDirect(decimal amount) => FormatReceipt(amount) - type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; source = Path('src/Billing/Pricing.cs').read_text(); tests = Path('tests/Billing.Tests/BillingTests.cs').read_text(); shim = re.search(r'private\s+string\s+RenderReceipt\s*\([^)]*\)\s*(?:=>|\{)(?P.*?)(?:;|\})', source, re.S); sys.exit(0 if shim and 'FormatReceipt' in shim.group('body') and 'InvokeConfigured(\"RenderReceipt\", 12.5m)' in tests else 1)" expected_exit_code: 0 timeout: 1m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Separates binding-based callers from the externally configured string-based lookup - Renames the implementation and ordinary caller while retaining a private old-name forwarding shim for reflection compatibility - Exercises the old configured name after the refactor instead of rewriting the compatibility test - Reports the runtime-name boundary and the validation evidence - name: Rename a helper used in every target-framework branch prompt: "Rename `PlatformInfo.Label` to `FormatLabel` everywhere it is declared and used, including every conditional-compilation branch, without changing the platform labels. Validate both target frameworks in Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/PlatformInfo.cs value: private static string FormatLabel - type: run-command config: command: >- python3 -c "from pathlib import Path; import re, sys; text = Path('src/Billing/PlatformInfo.cs').read_text(); sys.exit(0 if not re.search(r'\bLabel\s*\(', text) else 1)" expected_exit_code: 0 timeout: 1m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --framework net8.0 --verbosity normal expected_exit_code: 0 timeout: 5m - type: run-command config: command: dotnet test Fixture.sln --framework net10.0 --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Updates the declaration and references in every #if branch rather than validating only the active/default target - Preserves the net8 and net10 platform-label values - Reports separate successful validation for both target frameworks - name: Extract shared discount logic without collapsing distinct inputs prompt: "Refactor the duplicated tier-discount calculations in `OrderProcessor.DoStuff` into one private `ApplyTierDiscount(decimal amount, string tier)` helper. Preserve the existing difference between the invoice total base and quote base, including shipping, and verify Fixture.sln." environment: *fixture-environment graders: - type: file-contains config: path: src/Billing/OrderProcessor.cs value: private static decimal ApplyTierDiscount(decimal amount, string tier) - type: run-command config: command: >- python3 -c "from pathlib import Path; import sys; text = Path('src/Billing/OrderProcessor.cs').read_text(); sys.exit(0 if 'ApplyTierDiscount(subtotal, tier)' in text and 'ApplyTierDiscount(quoteBase, tier)' in text and text.count('tier == \"gold\"') == 1 and text.count('tier == \"silver\"') == 1 else 1)" expected_exit_code: 0 timeout: 1m - *fixture-integrity - type: run-command config: command: dotnet test Fixture.sln --verbosity normal expected_exit_code: 0 timeout: 5m - type: prompt rubric: - Extracts the repeated discount algorithm while passing subtotal and quoteBase through separately - Preserves shipping inclusion, decimal arithmetic, rounding order, and invoice results - Avoids collapsing the two calculations into one result merely because their tier-rate selection is duplicated - Reports the preserved semantic difference and actual validation result - name: Stay dormant for an ordinary feature request prompt: Add a platinum loyalty tier at 15% off and make it available to orders. expect_activation: false graders: - type: prompt rubric: - Treats the request as ordinary feature work rather than applying a behavior-preserving refactoring workflow - The specialized refactoring workflow stayed dormant and did not hijack the feature request