From a009e239f4445333b0d189333b059860f569d963 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 17 Jan 2026 11:06:14 -0800 Subject: [PATCH 1/3] Refactor ViewFile to flatten control flow with helper extraction and add fuzzy match notifications --- docs/C#-Coding-Style-Essential.md | 26 +++ docs/C#-Coding-Style-Expanded.md | 159 ++++++++++++++++++ src/common/Helpers/FileHelpers.cs | 92 ++++++++++ .../StrReplaceEditorHelperFunctions.cs | 18 +- 4 files changed, 292 insertions(+), 3 deletions(-) diff --git a/docs/C#-Coding-Style-Essential.md b/docs/C#-Coding-Style-Essential.md index 9a2dcde3..7b373b30 100644 --- a/docs/C#-Coding-Style-Essential.md +++ b/docs/C#-Coding-Style-Essential.md @@ -812,6 +812,30 @@ public DeliveryConfirmation ProcessDeliveryRequest(DeliveryRequest request) return DeliveryConfirmation.Successful(); } + +// Reduce nesting with helper extraction +public string ProcessFile(string path, bool applyFilters = true) +{ + var fileExists = File.Exists(path); + if (fileExists) return ProcessExistingFile(path, applyFilters); + + var fallbackPath = TryFindAlternative(path); + var fileNotFound = fallbackPath == null; + if (fileNotFound) return "File not found"; + + Logger.Info($"Using alternative: {fallbackPath}"); + return ProcessExistingFile(fallbackPath, applyFilters); +} + +private string ProcessExistingFile(string path, bool applyFilters) +{ + // All file processing logic extracted here + // Called by both direct and fallback paths + var content = File.ReadAllText(path); + return applyFilters ? ApplyFormatting(content) : content; +} + + // Use ternary for returns - single line for very simple cases public string GetDeliveryStatus(Package package) { @@ -829,12 +853,14 @@ public string GetDeliveryInstructions(DeliveryAddress address) ### Principles: - Use early returns to reduce nesting and improve readability +- Extract complex processing to private helpers when validation leads to nested logic - Use ternary operators for simple conditional return values - Return empty collections instead of null for collection results - Use expression-bodied methods for very simple returns ### Notes: - Early returns make code more readable by reducing indentation levels +- Helper extraction keeps validation/routing flat while enabling code reuse - Consistent return types make your APIs more predictable ## 17. Parameter Handling diff --git a/docs/C#-Coding-Style-Expanded.md b/docs/C#-Coding-Style-Expanded.md index 1fc85768..b7d98f7a 100644 --- a/docs/C#-Coding-Style-Expanded.md +++ b/docs/C#-Coding-Style-Expanded.md @@ -3538,6 +3538,35 @@ public OptimizedRoute PlanDeliveryRoute(List packages, DeliveryVehicle return OptimizedRoute.Successful(routeStops, estimatedDuration, fuelConsumption); } + + +// Delivery hub organization - reducing routing complexity with specialized processing centers +public string ProcessPackage(string trackingNumber) +{ + // Main delivery hub check - package already in system + var packageExists = _warehouse.HasPackage(trackingNumber); + if (packageExists) return ProcessExistingPackage(trackingNumber); + + // Backup hub search - check alternative storage facilities (TOP LEVEL - no extra nesting) + var alternativeLocation = _backupWarehouse.FindPackage(trackingNumber); + var packageNotFound = alternativeLocation == null; + if (packageNotFound) return "Package not found in any facility"; + + // Found in backup facility - use same processing workflow + Logger.Info($"Package located in backup facility: {alternativeLocation}"); + return ProcessExistingPackage(alternativeLocation); +} + +private string ProcessExistingPackage(string location) +{ + // Centralized package processing - used by both main and backup routes + // Scan package, verify contents, update tracking, prepare for delivery + var package = _packageService.Retrieve(location); + var scanResult = _scannerService.VerifyContents(package); + _trackingService.UpdateStatus(package.TrackingNumber, "Processing"); + return $"Package {package.TrackingNumber} ready for delivery from {location}"; +} + ``` ### Core Principles @@ -3562,6 +3591,15 @@ Think of method returns as delivery confirmations and package manifests from a p - Make return values self-documenting through clear property names and status indicators - Provide actionable information that helps callers respond appropriately to different outcomes + +**Specialized Processing Centers (Helper Extraction):** +- Extract complex package processing to dedicated facilities (private helpers) when validation leads to nested operations +- Keep routing logic flat at the hub level - main delivery path and backup path both visible at same level +- Centralize processing in specialized centers that multiple routes can use +- Route selection happens at top level with zero extra nesting - hub checks package location then routes to processing +- Processing centers handle all actual work - validation layer just decides which center to use + + ### Why It Matters Imagine a delivery service that provides unclear confirmations, inconsistent status reports, and unpredictable responses to delivery problems. Customers would never know if packages were delivered, rejected, or lost, making the service unreliable and frustrating to use. @@ -3871,6 +3909,127 @@ public class DeliveryProcessor } ``` + + +#### Evolution: Reducing Nesting with Helper Extraction + +Let's see how a method with nested validation logic can evolve into a flat, maintainable structure: + +**Initial Version - Deeply nested delivery routing:** + +```csharp +// BAD: Complex routing logic buried inside validation +public string RoutePackage(string trackingNumber) +{ + var packageInMainHub = _mainWarehouse.HasPackage(trackingNumber); + if (!packageInMainHub) + { + // NESTED: Fallback logic indented inside validation + var alternateLocation = _backupWarehouse.FindPackage(trackingNumber); + if (alternateLocation != null) + { + // DOUBLE NESTED: Processing logic further indented + var package = _packageService.Retrieve(alternateLocation); + var scanResult = _scannerService.VerifyContents(package); + _trackingService.UpdateStatus(package.TrackingNumber, "Processing"); + LogInfo($"Retrieved from backup: {alternateLocation}"); + return $"Package ready from {alternateLocation}"; + } + else + { + return "Package not found"; + } + } + + // Main processing at end, separated from fallback by many lines + var mainPackage = _packageService.Retrieve(trackingNumber); + var mainScan = _scannerService.VerifyContents(mainPackage); + _trackingService.UpdateStatus(mainPackage.TrackingNumber, "Processing"); + return $"Package ready from main hub"; +} +``` + +**Problems:** +- Processing logic duplicated in two places +- Fallback path heavily nested (two levels deep) +- Hard to see both paths at same level +- Main path and fallback path not clearly separated + +**Intermediate Version - Extract duplication but still nested:** + +```csharp +// BETTER: Extracted processing but routing still nested +public string RoutePackage(string trackingNumber) +{ + var packageInMainHub = _mainWarehouse.HasPackage(trackingNumber); + if (!packageInMainHub) + { + // STILL NESTED: Fallback logic indented + var alternateLocation = _backupWarehouse.FindPackage(trackingNumber); + if (alternateLocation != null) + { + LogInfo($"Retrieved from backup: {alternateLocation}"); + return ProcessPackage(alternateLocation); // Helper reduces duplication + } + return "Package not found"; + } + + return ProcessPackage(trackingNumber); // Main path uses same helper +} + +private string ProcessPackage(string location) +{ + var package = _packageService.Retrieve(location); + var scanResult = _scannerService.VerifyContents(package); + _trackingService.UpdateStatus(package.TrackingNumber, "Processing"); + return $"Package ready from {location}"; +} +``` + +**Better, but:** +- Fallback logic still nested inside validation +- Can't see both routes at the same indentation level +- Processing helper is good, but routing could be flatter + +**Final Version - Flat routing with helper extraction:** + +```csharp +// EXCELLENT: Flat routing, clear paths, reusable processing +public string RoutePackage(string trackingNumber) +{ + // Direct route: package in main hub + var packageInMainHub = _mainWarehouse.HasPackage(trackingNumber); + if (packageInMainHub) return ProcessPackage(trackingNumber); + + // Backup route: search alternative facilities (TOP LEVEL - no nesting) + var alternateLocation = _backupWarehouse.FindPackage(trackingNumber); + var packageNotFound = alternateLocation == null; + if (packageNotFound) return "Package not found in any facility"; + + // Found in backup - use same processing workflow + LogInfo($"Package located in backup facility: {alternateLocation}"); + return ProcessPackage(alternateLocation); +} + +private string ProcessPackage(string location) +{ + // Centralized processing - called by both routes + var package = _packageService.Retrieve(location); + var scanResult = _scannerService.VerifyContents(package); + _trackingService.UpdateStatus(package.TrackingNumber, "Processing"); + return $"Package ready from {location}"; +} +``` + +**Wins:** +- Both routing paths visible at same indentation level (zero extra nesting) +- Validation and routing stays flat and clear +- Processing logic centralized and reusable +- Each function has single responsibility: RoutePackage = routing, ProcessPackage = processing +- Easy to add more routing paths without increasing complexity + +**Key Insight:** When validation leads to complex processing, don't nest the processing inside the validation. Instead, use early returns to route to a specialized processing helper that both paths can share. + ### Deeper Understanding #### Delivery Confirmation Patterns diff --git a/src/common/Helpers/FileHelpers.cs b/src/common/Helpers/FileHelpers.cs index bce34cf3..e6a578d3 100644 --- a/src/common/Helpers/FileHelpers.cs +++ b/src/common/Helpers/FileHelpers.cs @@ -684,5 +684,97 @@ private static char[] GetInvalidFileNameCharsForWeb() return null; } + /// + /// Attempts to find a file using progressive fuzzy matching when the exact path doesn't exist. + /// Progressively relaxes path matching from right-to-left by adding wildcards. + /// + /// The originally requested file path that doesn't exist + /// The matched file path if exactly one match is found, null otherwise + public static string? TryFuzzyFindFile(string requestedPath) + { + // Already checked that exact path doesn't exist before calling this + requestedPath = PathHelpers.ExpandPath(requestedPath); + + // Split path into segments + var segments = requestedPath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + + if (segments.Length == 0) return null; + + // Strategy: Progressively add * suffix to segments from right-to-left + // Example: a/b/c/file.cs -> a/b/c*/file.cs -> a/b/*/file.cs -> a/*/*/file.cs -> **/file.cs + + for (int wildcardIndex = segments.Length - 2; wildcardIndex >= 0; wildcardIndex--) + { + // Build pattern with * suffix on segments from wildcardIndex onwards (except the filename) + var patternSegments = new string[segments.Length]; + for (int i = 0; i < segments.Length; i++) + { + if (i >= wildcardIndex && i < segments.Length - 1) + { + // Replace this segment with * + patternSegments[i] = "*"; + } + else + { + patternSegments[i] = segments[i]; + } + } + + var pattern = string.Join(Path.DirectorySeparatorChar.ToString(), patternSegments); + Logger.Verbose($"TryFuzzyFindFile: Trying pattern: {pattern}"); + + try + { + var matches = FindFiles(pattern).ToList(); + + if (matches.Count == 1) + { + Logger.Info($"TryFuzzyFindFile: Found unique match for '{requestedPath}' -> '{matches[0]}'"); + return matches[0]; + } + else if (matches.Count > 1) + { + Logger.Verbose($"TryFuzzyFindFile: Pattern '{pattern}' matched {matches.Count} files (ambiguous)"); + // Keep trying more specific patterns + } + } + catch (Exception ex) + { + Logger.Verbose($"TryFuzzyFindFile: Exception trying pattern '{pattern}': {ex.Message}"); + } + } + + // Last resort: try filename with ** (search anywhere) + var filename = segments[segments.Length - 1]; + var lastResortPattern = $"**{Path.DirectorySeparatorChar}{filename}"; + Logger.Verbose($"TryFuzzyFindFile: Trying last resort pattern: {lastResortPattern}"); + + try + { + var matches = FindFiles(lastResortPattern).ToList(); + + if (matches.Count == 1) + { + Logger.Info($"TryFuzzyFindFile: Found unique match with last resort for '{requestedPath}' -> '{matches[0]}'"); + return matches[0]; + } + else if (matches.Count > 1) + { + Logger.Info($"TryFuzzyFindFile: Last resort pattern matched {matches.Count} files, suggestions:"); + foreach (var match in matches.Take(5)) + { + Logger.Info($" - {match}"); + } + } + } + catch (Exception ex) + { + Logger.Verbose($"TryFuzzyFindFile: Exception trying last resort pattern: {ex.Message}"); + } + + Logger.Verbose($"TryFuzzyFindFile: No unique match found for '{requestedPath}'"); + return null; + } + private static char[] _invalidFileNameCharsForWeb = GetInvalidFileNameCharsForWeb(); } diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index e5c851f7..a72c44a2 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -58,10 +58,22 @@ public string ViewFile( { path = PathHelpers.ExpandPath(path); - // Basic file validation - var noFile = Directory.Exists(path) || !File.Exists(path); - if (noFile) return $"Path {path} does not exist or is not a file."; + var fileExists = File.Exists(path) && !Directory.Exists(path); + if (fileExists) return ViewExistingFile(path, startLine, endLine, lineNumbers, lineContains, removeAllLines, linesBeforeAndAfter, maxCharsPerLine, maxTotalChars); + + var fuzzyPath = FileHelpers.TryFuzzyFindFile(path); + + var fileNotFound = fuzzyPath == null; + if (fileNotFound) return $"Path {path} does not exist or is not a file."; + + Logger.Info($"ViewFile: Fuzzy matched '{path}' to '{fuzzyPath}'"); + var content = ViewExistingFile(fuzzyPath!, startLine, endLine, lineNumbers, lineContains, removeAllLines, linesBeforeAndAfter, maxCharsPerLine, maxTotalChars); + return $"Note: Fuzzy matched '{path}' to '{fuzzyPath}'\n\n{content}"; + } + + private string ViewExistingFile(string path, int startLine, int endLine, bool lineNumbers, string lineContains, string removeAllLines, int linesBeforeAndAfter, int maxCharsPerLine, int maxTotalChars) + { // Read all lines from file var allLines = FileHelpers.ReadAllText(path).Split('\n', StringSplitOptions.None) .Select(line => line.TrimEnd('\r')) From e28a9b98b009f1aa0127e0ab4b0e6194a355f36a Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 17 Jan 2026 11:54:46 -0800 Subject: [PATCH 2/3] Add fuzzy file matching to ReplaceFileContent, ReplaceOneInFile, ReplaceMultipleInFile, and Insert --- .../StrReplaceEditorHelperFunctions.cs | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs index a72c44a2..77116553 100644 --- a/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs +++ b/src/cycod/FunctionCallingTools/StrReplaceEditorHelperFunctions.cs @@ -329,12 +329,22 @@ public string ReplaceFileContent( [Description("New content to replace the entire file.")] string newContent, [Description("Current line count of the file (for verification).")] int oldContentLineCount) { + path = PathHelpers.ExpandPath(path); - if (!File.Exists(path)) - { - return $"File {path} does not exist. Use CreateFile to create a new file."; - } + var fileExists = File.Exists(path); + if (fileExists) return ReplaceExistingFileContent(path, newContent, oldContentLineCount); + + var fuzzyPath = FileHelpers.TryFuzzyFindFile(path); + var fileNotFound = fuzzyPath == null; + if (fileNotFound) return $"File {path} does not exist. Use CreateFile to create a new file."; + + Logger.Info($"ReplaceFileContent: Fuzzy matched '{path}' to '{fuzzyPath}'"); + var result = ReplaceExistingFileContent(fuzzyPath!, newContent, oldContentLineCount); + return $"Note: Fuzzy matched '{path}' to '{fuzzyPath}'\n\n{result}"; + } + private string ReplaceExistingFileContent(string path, string newContent, int oldContentLineCount) + { // Read current content and count lines var currentContent = File.ReadAllText(path); var currentLines = currentContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); @@ -410,12 +420,22 @@ public string ReplaceOneInFile( [Description("Existing text in the file that should be replaced. Must match exactly one occurrence.")] string oldStr, [Description("New string content that will replace the old string.")] string newStr) { + path = PathHelpers.ExpandPath(path); - if (!File.Exists(path)) - { - return $"File {path} does not exist."; - } + var fileExists = File.Exists(path); + if (fileExists) return ReplaceOneInExistingFile(path, oldStr, newStr); + + var fuzzyPath = FileHelpers.TryFuzzyFindFile(path); + var fileNotFound = fuzzyPath == null; + if (fileNotFound) return $"File {path} does not exist."; + Logger.Info($"ReplaceOneInFile: Fuzzy matched '{path}' to '{fuzzyPath}'"); + var result = ReplaceOneInExistingFile(fuzzyPath!, oldStr, newStr); + return $"Note: Fuzzy matched '{path}' to '{fuzzyPath}'\n\n{result}"; + } + + private string ReplaceOneInExistingFile(string path, string oldStr, string newStr) + { var text = FileHelpers.ReadAllText(path); var replaced = StringHelpers.ReplaceOnce(text, oldStr, newStr, out var countFound); if (countFound != 1) @@ -441,12 +461,22 @@ public string ReplaceMultipleInFile( [Description("Array of old strings to be replaced. Each must match exactly one occurrence.")] string[] oldStrings, [Description("Array of new strings to replace with. Must be same length as oldStrings.")] string[] newStrings) { + path = PathHelpers.ExpandPath(path); - if (!File.Exists(path)) - { - return $"File {path} does not exist."; - } + var fileExists = File.Exists(path); + if (fileExists) return ReplaceMultipleInExistingFile(path, oldStrings, newStrings); + + var fuzzyPath = FileHelpers.TryFuzzyFindFile(path); + var fileNotFound = fuzzyPath == null; + if (fileNotFound) return $"File {path} does not exist."; + + Logger.Info($"ReplaceMultipleInFile: Fuzzy matched '{path}' to '{fuzzyPath}'"); + var result = ReplaceMultipleInExistingFile(fuzzyPath!, oldStrings, newStrings); + return $"Note: Fuzzy matched '{path}' to '{fuzzyPath}'\n\n{result}"; + } + private string ReplaceMultipleInExistingFile(string path, string[] oldStrings, string[] newStrings) + { if (oldStrings.Length != newStrings.Length) { return $"Error: oldStrings array length ({oldStrings.Length}) must match newStrings array length ({newStrings.Length})."; @@ -511,11 +541,22 @@ public string Insert( [Description("Line number (1-indexed) after which to insert the new string.")] int insertLine, [Description("The string to insert into the file.")] string newStr) { + path = PathHelpers.ExpandPath(path); - if (!File.Exists(path)) - { - return $"File {path} does not exist."; - } + var fileExists = File.Exists(path); + if (fileExists) return InsertIntoExistingFile(path, insertLine, newStr); + + var fuzzyPath = FileHelpers.TryFuzzyFindFile(path); + var fileNotFound = fuzzyPath == null; + if (fileNotFound) return $"File {path} does not exist."; + + Logger.Info($"Insert: Fuzzy matched '{path}' to '{fuzzyPath}'"); + var result = InsertIntoExistingFile(fuzzyPath!, insertLine, newStr); + return $"Note: Fuzzy matched '{path}' to '{fuzzyPath}'\n\n{result}"; + } + + private string InsertIntoExistingFile(string path, int insertLine, string newStr) + { var text = FileHelpers.ReadAllText(path); var lines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList(); if (insertLine < 0 || insertLine > lines.Count) From d6298c2c7d6dc8958fa94d57c377757a171aaf15 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Sat, 17 Jan 2026 12:29:59 -0800 Subject: [PATCH 3/3] Add bash-style path normalization to handle /c/ style paths from shell output --- src/common/Helpers/FileHelpers.cs | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/common/Helpers/FileHelpers.cs b/src/common/Helpers/FileHelpers.cs index e6a578d3..d52538f8 100644 --- a/src/common/Helpers/FileHelpers.cs +++ b/src/common/Helpers/FileHelpers.cs @@ -684,6 +684,36 @@ private static char[] GetInvalidFileNameCharsForWeb() return null; } + /// + /// Normalizes bash-style paths (e.g., /c/folder/file.txt) to Windows paths (e.g., C:\folder\file.txt). + /// On Windows, Git Bash uses /c/ to represent C:\, /d/ for D:\, etc. + /// + /// The path to normalize + /// The normalized path, or the original path if no normalization needed + private static string NormalizeBashStylePath(string path) + { + if (string.IsNullOrEmpty(path)) return path; + + // Check for bash-style paths: /c/, /d/, etc. (Unix-style root with single letter) + // Pattern: starts with / followed by single letter followed by / + if (path.Length >= 3 && + path[0] == '/' && + char.IsLetter(path[1]) && + path[2] == '/') + { + var driveLetter = char.ToUpper(path[1]); + var remainingPath = path.Substring(3); // Skip "/c/" + + // Convert forward slashes to backslashes + remainingPath = remainingPath.Replace('/', Path.DirectorySeparatorChar); + + return $"{driveLetter}:{Path.DirectorySeparatorChar}{remainingPath}"; + } + + return path; + } + + /// /// Attempts to find a file using progressive fuzzy matching when the exact path doesn't exist. /// Progressively relaxes path matching from right-to-left by adding wildcards. @@ -695,6 +725,23 @@ private static char[] GetInvalidFileNameCharsForWeb() // Already checked that exact path doesn't exist before calling this requestedPath = PathHelpers.ExpandPath(requestedPath); + // Normalize bash-style paths (e.g., /c/folder -> C:\folder) + var normalizedPath = NormalizeBashStylePath(requestedPath); + if (normalizedPath != requestedPath) + { + Logger.Verbose($"TryFuzzyFindFile: Normalized bash-style path '{requestedPath}' -> '{normalizedPath}'"); + + // Check if the normalized path exists exactly + if (File.Exists(normalizedPath)) + { + Logger.Info($"TryFuzzyFindFile: Found file after bash path normalization '{requestedPath}' -> '{normalizedPath}'"); + return normalizedPath; + } + + // Use the normalized path for fuzzy matching + requestedPath = normalizedPath; + } + // Split path into segments var segments = requestedPath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);