diff --git a/install.ps1 b/install.ps1 index ed02bb7..ebf764a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -68,6 +68,7 @@ if ($CreateDesktopShortcut) { $Shortcut.IconLocation = "$InstallPath\Envoy.exe,0" $Shortcut.Description = "Envoy - Job Application Agent" $Shortcut.Save() + [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($WshShell) } if ($CreateStartMenuShortcut) { @@ -82,6 +83,7 @@ if ($CreateStartMenuShortcut) { $Shortcut.IconLocation = "$InstallPath\Envoy.exe,0" $Shortcut.Description = "Envoy - Job Application Agent" $Shortcut.Save() + [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($WshShell) } # Create uninstall registry entry diff --git a/publish.ps1 b/publish.ps1 index 84d9547..2c26b32 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -85,7 +85,7 @@ if (Test-Path "THIRD-PARTY-NOTICES.md") { Write-Host "Creating launcher script..." -ForegroundColor Yellow $StartScriptContent = "@echo off`nchcp 65001 >nul`necho.`necho Envoy Job Application Agent`necho.`necho Starting Envoy...`necho.`n`n:: Check if Ollama is running (optional - only needed for local models)`ncurl -s http://localhost:11434 >nul 2>&1`nif errorlevel 1 (`n echo Note: Ollama is not running. That's fine if you only use cloud LLM providers.`n echo For local LLMs install Ollama: https://ollama.com/download`n echo.`n)`n`n:: Start Envoy`nstart `"`" `"%~dp0Envoy.exe`"" -$StartScriptContent | Out-File -FilePath "$PublishPath/Start Envoy.bat" -Encoding UTF8 +$StartScriptContent | Out-File -FilePath "$PublishPath/Start Envoy.bat" -Encoding ascii # Create README Write-Host "Creating package README..." -ForegroundColor Yellow diff --git a/src/Envoy.Core/Services/CdpBrowserService.cs b/src/Envoy.Core/Services/CdpBrowserService.cs index a0f803f..bc401f0 100644 --- a/src/Envoy.Core/Services/CdpBrowserService.cs +++ b/src/Envoy.Core/Services/CdpBrowserService.cs @@ -160,8 +160,8 @@ public async Task TypeTextAsync(string nodeId, string text, CancellationToken ct int vkCode; if (char.IsLetter(ch)) { - code = $"Key{char.ToUpper(ch)}"; - vkCode = (int)char.ToUpper(ch); + code = $"Key{char.ToUpperInvariant(ch)}"; + vkCode = (int)char.ToUpperInvariant(ch); } else if (char.IsDigit(ch)) { @@ -433,7 +433,7 @@ public async Task WaitForEventAsync(string eventName, TimeSpan timeout, Cancella private async Task ReceiveLoop(CancellationToken ct) { var buffer = new byte[131072]; - var messageBuilder = new MemoryStream(); + using var messageBuilder = new MemoryStream(); while (!ct.IsCancellationRequested && _webSocket?.State == WebSocketState.Open) { diff --git a/src/Envoy.Core/Services/DomScorer.cs b/src/Envoy.Core/Services/DomScorer.cs index d827d16..a3e07ca 100644 --- a/src/Envoy.Core/Services/DomScorer.cs +++ b/src/Envoy.Core/Services/DomScorer.cs @@ -71,7 +71,7 @@ public static class DomScorer foreach (var candidate in matchable) { var score = ScoreCandidate(fingerprint, candidate); - if (score >= bestScore) + if (score > bestScore) { bestScore = score; best = candidate; diff --git a/src/Envoy.Core/Services/OllamaService.cs b/src/Envoy.Core/Services/OllamaService.cs index af3ece8..fdb0793 100644 --- a/src/Envoy.Core/Services/OllamaService.cs +++ b/src/Envoy.Core/Services/OllamaService.cs @@ -22,6 +22,7 @@ public OllamaService(ILLMProvider provider, ILogger? log = null) public void SwitchProvider(ILLMProvider newProvider) { + ArgumentNullException.ThrowIfNull(newProvider); _activeProvider = newProvider; _log.LogInformation("Switched LLM provider to {Provider} ({ProviderId})", newProvider.DisplayName, newProvider.ProviderId); } diff --git a/src/Envoy.Core/Services/RelocationLogger.cs b/src/Envoy.Core/Services/RelocationLogger.cs index 6a64112..35e2506 100644 --- a/src/Envoy.Core/Services/RelocationLogger.cs +++ b/src/Envoy.Core/Services/RelocationLogger.cs @@ -56,7 +56,6 @@ public Task LogAsync(RelocationEntry entry) // to PR against. Surface it. _log?.LogWarning(ex, "Failed to write relocation entry for template {TemplateId} field {Field}", entry.TemplateId, entry.Field); - System.Diagnostics.Debug.WriteLine($"[RelocationLogger] Write failed: {ex.Message}"); } }); } diff --git a/src/Envoy.Core/Services/SafetyService.cs b/src/Envoy.Core/Services/SafetyService.cs index 61a6972..6d6fd72 100644 --- a/src/Envoy.Core/Services/SafetyService.cs +++ b/src/Envoy.Core/Services/SafetyService.cs @@ -236,7 +236,7 @@ private void CheckKeywordDensity(MasterProfile tailored, string jobDescription, if (resumeWords.Length == 0) return; - var matchCount = jdWords.Count(jd => resumeWords.Any(r => r.Contains(jd, StringComparison.OrdinalIgnoreCase))); + var matchCount = jdWords.Count(jd => resumeWords.Any(r => r.Equals(jd, StringComparison.OrdinalIgnoreCase))); var density = (double)matchCount / jdWords.Count; // 30% threshold catches LLM-driven stuffing without flagging the natural @@ -318,7 +318,7 @@ private static List ExtractKeywords(string text) { var words = text.Split(new[] { ' ', '\n', '\r', '.', ',', ';' }, StringSplitOptions.RemoveEmptyEntries); var stopWords = new HashSet { "the", "and", "for", "with", "you", "are", "will", "must", "have", "this", "that" }; - return words.Where(w => w.Length > 3 && !stopWords.Contains(w.ToLower())).ToList(); + return words.Where(w => w.Length > 3 && !stopWords.Contains(w.ToLowerInvariant())).ToList(); } private static string SerializeResume(MasterProfile profile) diff --git a/src/Envoy.Core/Services/TailoringEngine.cs b/src/Envoy.Core/Services/TailoringEngine.cs index cc2fb39..dc4cb6d 100644 --- a/src/Envoy.Core/Services/TailoringEngine.cs +++ b/src/Envoy.Core/Services/TailoringEngine.cs @@ -131,8 +131,8 @@ private static string TruncateAtSentenceBoundary(string text, int maxLength) private double CalculateMatchScore(MasterProfile tailored, string jobDescription) { - var resumeText = System.Text.Json.JsonSerializer.Serialize(tailored).ToLower(); - var jdWords = jobDescription.ToLower().Split(new[] { ' ', '\n', '\r', '.', ',' }, StringSplitOptions.RemoveEmptyEntries) + var resumeText = System.Text.Json.JsonSerializer.Serialize(tailored).ToLowerInvariant(); + var jdWords = jobDescription.ToLowerInvariant().Split(new[] { ' ', '\n', '\r', '.', ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(w => w.Length > 3) .Distinct() .ToList(); diff --git a/src/Envoy.Core/Services/TemplateEngine.cs b/src/Envoy.Core/Services/TemplateEngine.cs index 11c7001..70ea7ad 100644 --- a/src/Envoy.Core/Services/TemplateEngine.cs +++ b/src/Envoy.Core/Services/TemplateEngine.cs @@ -93,7 +93,7 @@ public async Task ExecuteTemplateAsync( { ct.ThrowIfCancellationRequested(); - switch (step.Action.ToLower()) + switch (step.Action.ToLowerInvariant()) { case "wait_for": await WaitForElementAsync(browser, step, ct); @@ -186,7 +186,7 @@ private static string GetValueFromProfile(TailoredProfile profile, string field) var data = profile.TailoredData; var name = data.Name ?? ""; var nameParts = name.Split(' ', 2); - var key = (field ?? "").ToLower(); + var key = (field ?? "").ToLowerInvariant(); switch (key) { diff --git a/src/Envoy.Discovery/SeedBoards.cs b/src/Envoy.Discovery/SeedBoards.cs index 7a0a62b..d7cd502 100644 --- a/src/Envoy.Discovery/SeedBoards.cs +++ b/src/Envoy.Discovery/SeedBoards.cs @@ -43,12 +43,12 @@ public static IReadOnlyList Load() return Default; } - public static readonly IReadOnlyList Default = new[] + public static readonly IReadOnlyList Default = Array.AsReadOnly(new[] { new AtsBoardRef { Ats = JobSource.Ashby, Token = "openai", CompanyName = "OpenAI" }, new AtsBoardRef { Ats = JobSource.Lever, Token = "matchgroup", CompanyName = "Match Group" }, new AtsBoardRef { Ats = JobSource.Lever, Token = "gopuff", CompanyName = "Gopuff" }, new AtsBoardRef { Ats = JobSource.Lever, Token = "veeva", CompanyName = "Veeva Systems" }, new AtsBoardRef { Ats = JobSource.Workable, Token = "viva", CompanyName = "Viva.com" }, - }; + }); } diff --git a/src/Envoy.GhostDetection/GhostScorer.cs b/src/Envoy.GhostDetection/GhostScorer.cs index f009a61..67d579b 100644 --- a/src/Envoy.GhostDetection/GhostScorer.cs +++ b/src/Envoy.GhostDetection/GhostScorer.cs @@ -82,7 +82,7 @@ public async Task ScoreAsync(JobPosting posting, CancellationToken c // ── TopEvidence: strongest lines, capped ───────────────────────── var topEvidence = results .OrderByDescending(r => r.Confidence * (r.Tier == SignalTier.Deterministic ? 10 : r.Tier == SignalTier.Probabilistic ? 3 : 1)) - .SelectMany(r => r.Evidence) + .SelectMany(r => r.Evidence ?? Array.Empty()) .Distinct() .Take(6) .ToArray(); diff --git a/src/Envoy.GhostDetection/Signals/AtsCrossCheckSignal.cs b/src/Envoy.GhostDetection/Signals/AtsCrossCheckSignal.cs index 884654f..cb2c283 100644 --- a/src/Envoy.GhostDetection/Signals/AtsCrossCheckSignal.cs +++ b/src/Envoy.GhostDetection/Signals/AtsCrossCheckSignal.cs @@ -129,8 +129,6 @@ public AtsCrossCheckSignal(HttpClient http) var segments = uri.Segments; for (int i = 0; i < segments.Length - 1; i++) { - if (segments[i].Trim('/').Equals("boards.greenhouse.io", StringComparison.OrdinalIgnoreCase)) - continue; if (segments[i].Trim('/').Equals("jobs", StringComparison.OrdinalIgnoreCase)) return null; var seg = segments[i].Trim('/'); @@ -152,8 +150,6 @@ public AtsCrossCheckSignal(HttpClient http) for (int i = 0; i < segments.Length; i++) { var seg = segments[i].Trim('/'); - if (seg.Equals("jobs.lever.co", StringComparison.OrdinalIgnoreCase)) - continue; if (!string.IsNullOrEmpty(seg)) return seg; } diff --git a/src/Envoy.GhostDetection/Signals/PostingAgeSignal.cs b/src/Envoy.GhostDetection/Signals/PostingAgeSignal.cs index a081415..726e15f 100644 --- a/src/Envoy.GhostDetection/Signals/PostingAgeSignal.cs +++ b/src/Envoy.GhostDetection/Signals/PostingAgeSignal.cs @@ -55,7 +55,7 @@ public class PostingAgeSignal : IGhostSignal private static Seniority InferSeniority(string title) { - var t = title.ToLowerInvariant(); + var t = (title ?? string.Empty).ToLowerInvariant(); // Executive / very senior if (ContainsAny(t, "vice president", "head of") || diff --git a/src/Envoy.UI/Theme.cs b/src/Envoy.UI/Theme.cs index e495d16..f3d8ace 100644 --- a/src/Envoy.UI/Theme.cs +++ b/src/Envoy.UI/Theme.cs @@ -17,6 +17,7 @@ internal static class Theme public static readonly SolidColorBrush Muted = Freeze(new SolidColorBrush(Color.FromRgb(0x88, 0x92, 0xA4))); public static readonly SolidColorBrush TextFg = Freeze(new SolidColorBrush(Color.FromRgb(0xE0, 0xE6, 0xF0))); public static readonly SolidColorBrush Surface = Freeze(new SolidColorBrush(Color.FromRgb(0x11, 0x18, 0x27))); + public static readonly SolidColorBrush CardSurface = Freeze(new SolidColorBrush(Color.FromRgb(0x1A, 0x22, 0x35))); public static readonly SolidColorBrush Background = Freeze(new SolidColorBrush(Color.FromRgb(0x0A, 0x0E, 0x17))); public static readonly SolidColorBrush BorderColor = Freeze(new SolidColorBrush(Color.FromRgb(0x1A, 0x3A, 0x4A))); public static readonly SolidColorBrush NavActiveBg = Freeze(new SolidColorBrush(Color.FromArgb(40, 0, 240, 255))); diff --git a/src/Envoy.UI/VaultView.xaml.cs b/src/Envoy.UI/VaultView.xaml.cs index 2c8fbfa..dac5a05 100644 --- a/src/Envoy.UI/VaultView.xaml.cs +++ b/src/Envoy.UI/VaultView.xaml.cs @@ -58,9 +58,9 @@ public async Task SetProfileId(Guid profileId) { var border = new Border { - Background = new SolidColorBrush(Color.FromRgb(0x1A, 0x22, 0x35)), + Background = CardSurface, CornerRadius = new CornerRadius(3), - BorderBrush = new SolidColorBrush(Color.FromRgb(0x1A, 0x3A, 0x4A)), + BorderBrush = BorderColor, BorderThickness = new Thickness(1), Padding = new Thickness(12), Margin = new Thickness(0, 0, 0, 8), @@ -83,7 +83,7 @@ public async Task SetProfileId(Guid profileId) ? string.Join("\n", _profile.Anomalies.Select(a => $"⚠ {a.Field}: {a.Message} [{a.Severity}]")) : "✓ NO ANOMALIES DETECTED"; AnomaliesText.Foreground = _profile.Anomalies.Any() - ? new SolidColorBrush(Color.FromRgb(0xFF, 0xE6, 0x00)) + ? Yellow : Green; SaveStatus.Text = ""; diff --git a/tests/Envoy.Discovery.Tests/DiscoveryContainerResolutionTests.cs b/tests/Envoy.Discovery.Tests/DiscoveryContainerResolutionTests.cs index aca2ab7..e1774a9 100644 --- a/tests/Envoy.Discovery.Tests/DiscoveryContainerResolutionTests.cs +++ b/tests/Envoy.Discovery.Tests/DiscoveryContainerResolutionTests.cs @@ -25,16 +25,16 @@ public void AddEnvoyDiscovery_ResolvesServiceAndAllSources() var discovery = provider.GetRequiredService(); Assert.NotNull(discovery); - var sources = provider.GetServices().ToList(); - Assert.Equal(5, sources.Count); - var expected = new[] { JobSource.Greenhouse, JobSource.Lever, JobSource.Ashby, JobSource.Workable, JobSource.Recruitee }; + + var sources = provider.GetServices().ToList(); + Assert.Equal(expected.Length, sources.Count); Assert.All(expected, ats => Assert.Contains(sources, s => s.Ats == ats)); var web = provider.GetRequiredService(); Assert.Equal("Brave Search", web.Name); Assert.NotEmpty(discovery.DefaultBoards); - Assert.Equal(5, discovery.SupportedAts.Count); + Assert.Equal(expected.Length, discovery.SupportedAts.Count); } }