refactor: Rename TrueCV to RealCV throughout codebase

- Renamed all directories (TrueCV.* -> RealCV.*)
- Renamed all project files (.csproj)
- Renamed solution file (TrueCV.sln -> RealCV.sln)
- Updated all namespaces in C# and Razor files
- Updated project references
- Updated CSS variable names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 20:47:55 +00:00
parent 6f384f8d09
commit 92a3b60878
107 changed files with 693 additions and 554 deletions

View File

@@ -0,0 +1,167 @@
using System.Text.Json;
using Anthropic.SDK;
using Anthropic.SDK.Messaging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RealCV.Application.Helpers;
using RealCV.Application.Interfaces;
using RealCV.Application.Models;
using RealCV.Infrastructure.Configuration;
using RealCV.Infrastructure.Helpers;
namespace RealCV.Infrastructure.Services;
public sealed class AICompanyNameMatcherService : ICompanyNameMatcherService
{
private readonly AnthropicClient _anthropicClient;
private readonly ILogger<AICompanyNameMatcherService> _logger;
private const string SystemPrompt = """
You are a UK company name matching expert. Your task is to determine if a company name
from a CV matches any of the official company names from Companies House records.
You understand:
- Trading names vs registered names (e.g., "Tesco" = "TESCO PLC")
- Subsidiaries vs parent companies (e.g., "ASDA" might work for "ASDA STORES LIMITED")
- Common abbreviations (Ltd = Limited, PLC = Public Limited Company, CiC = Community Interest Company)
- That completely different words mean different companies (e.g., "Families First" "Families Against Conformity")
You must respond ONLY with valid JSON, no other text or markdown.
""";
private const string MatchingPrompt = """
Compare the company name from a CV against official Companies House records.
CV Company Name: "{CV_COMPANY}"
Companies House Candidates:
{CANDIDATES}
Determine which candidate (if any) is the SAME company as the CV entry.
Rules:
1. A match requires the companies to be the SAME organisation, not just similar names
2. "Families First CiC" is NOT the same as "FAMILIES AGAINST CONFORMITY LTD" - different words = different companies
3. Trading names should match their registered entity (e.g., "Tesco" matches "TESCO PLC")
4. Subsidiaries can match if clearly the same organisation (e.g., "ASDA" could match "ASDA STORES LIMITED")
5. Acronyms in parentheses are abbreviations of the full name (e.g., "North Halifax Partnership (NHP)" = "NORTH HALIFAX PARTNERSHIP")
6. CiC/CIC = Community Interest Company, LLP = Limited Liability Partnership - these are legal suffixes
7. If the CV name contains all the key words of a candidate (ignoring Ltd/Limited/CIC/etc.), it's likely a match
8. If NO candidate is clearly the same company, return "NONE" as the best match
Respond with this exact JSON structure:
{
"bestMatchCompanyNumber": "string (company number of best match, or 'NONE' if no valid match)",
"confidenceScore": number (0-100, where 100 = certain match, 0 = no match),
"matchType": "string (Exact, TradingName, Subsidiary, Parent, NoMatch)",
"reasoning": "string (brief explanation of why this is or isn't a match)"
}
""";
public AICompanyNameMatcherService(
IOptions<AnthropicSettings> settings,
ILogger<AICompanyNameMatcherService> logger)
{
_logger = logger;
_anthropicClient = new AnthropicClient(settings.Value.ApiKey);
}
public async Task<SemanticMatchResult?> FindBestMatchAsync(
string cvCompanyName,
List<CompanyCandidate> candidates,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(cvCompanyName) || candidates.Count == 0)
{
return null;
}
_logger.LogDebug("Using AI to match '{CVCompany}' against {Count} candidates",
cvCompanyName, candidates.Count);
try
{
var candidatesText = string.Join("\n", candidates.Select((c, i) =>
$"{i + 1}. {c.CompanyName} (Number: {c.CompanyNumber}, Status: {c.CompanyStatus ?? "Unknown"})"));
var prompt = MatchingPrompt
.Replace("{CV_COMPANY}", cvCompanyName)
.Replace("{CANDIDATES}", candidatesText);
var messages = new List<Message>
{
new(RoleType.User, prompt)
};
var parameters = new MessageParameters
{
Model = "claude-sonnet-4-20250514",
MaxTokens = 1024,
Messages = messages,
System = [new SystemMessage(SystemPrompt)]
};
var response = await _anthropicClient.Messages.GetClaudeMessageAsync(parameters, cancellationToken);
var responseText = response.Content
.OfType<TextContent>()
.FirstOrDefault()?.Text;
if (string.IsNullOrWhiteSpace(responseText))
{
_logger.LogWarning("AI returned empty response for company matching");
return null;
}
responseText = JsonResponseHelper.CleanJsonResponse(responseText);
var aiResponse = JsonSerializer.Deserialize<AIMatchResponse>(responseText, JsonDefaults.CamelCase);
if (aiResponse is null)
{
_logger.LogWarning("Failed to deserialize AI response: {Response}", responseText);
return null;
}
_logger.LogDebug("AI match result: {CompanyNumber} with {Score}% confidence - {Reasoning}",
aiResponse.BestMatchCompanyNumber, aiResponse.ConfidenceScore, aiResponse.Reasoning);
// Find the matched candidate
if (aiResponse.BestMatchCompanyNumber == "NONE" || aiResponse.ConfidenceScore < 50)
{
return new SemanticMatchResult
{
CandidateCompanyName = "No match",
CandidateCompanyNumber = "NONE",
ConfidenceScore = 0,
MatchType = "NoMatch",
Reasoning = aiResponse.Reasoning
};
}
var matchedCandidate = candidates.FirstOrDefault(c =>
c.CompanyNumber.Equals(aiResponse.BestMatchCompanyNumber, StringComparison.OrdinalIgnoreCase));
if (matchedCandidate is null)
{
_logger.LogWarning("AI returned company number {Number} not in candidates list",
aiResponse.BestMatchCompanyNumber);
return null;
}
return new SemanticMatchResult
{
CandidateCompanyName = matchedCandidate.CompanyName,
CandidateCompanyNumber = matchedCandidate.CompanyNumber,
ConfidenceScore = aiResponse.ConfidenceScore,
MatchType = aiResponse.MatchType,
Reasoning = aiResponse.Reasoning
};
}
catch (Exception ex)
{
_logger.LogError(ex, "AI company matching failed for '{CVCompany}'", cvCompanyName);
return null; // Fall back to fuzzy matching
}
}
}