feat(career): add factual evidence bank

This commit is contained in:
cesnimda
2026-08-31 17:03:09 +02:00
parent 5337ba3a5e
commit 78ad0c66f9
16 changed files with 362 additions and 5 deletions
@@ -138,6 +138,7 @@ public sealed class AccountDataExportService(
var careerProjects = await db.CareerProjects.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.SortOrder).ToListAsync(cancellationToken);
var careerCertifications = await db.CareerCertifications.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.SortOrder).ToListAsync(cancellationToken);
var careerLanguages = await db.CareerLanguages.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.SortOrder).ToListAsync(cancellationToken);
var careerEvidence = await db.CareerEvidence.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.Id).ToListAsync(cancellationToken);
await AddJsonAsync("data/career.json", new
{
Profile = careerProfile,
@@ -148,7 +149,8 @@ public sealed class AccountDataExportService(
Projects = careerProjects,
Certifications = careerCertifications,
Languages = careerLanguages,
}, (careerProfileId is null ? 0 : 1) + careerVersions.Count + careerExperiences.Count + careerEducation.Count + careerSkills.Count + careerProjects.Count + careerCertifications.Count + careerLanguages.Count);
Evidence = careerEvidence,
}, (careerProfileId is null ? 0 : 1) + careerVersions.Count + careerExperiences.Count + careerEducation.Count + careerSkills.Count + careerProjects.Count + careerCertifications.Count + careerLanguages.Count + careerEvidence.Count);
var variants = await db.CvVariants.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.Id).ToListAsync(cancellationToken);
var variantVersions = await db.CvVariantVersions.IgnoreQueryFilters().AsNoTracking().Where(item => item.OwnerUserId == ownerUserId).OrderBy(item => item.Id).ToListAsync(cancellationToken);
@@ -248,6 +248,7 @@ public sealed class AccountDeletionService(
deleted += await db.CareerProjects.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CareerCertifications.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CareerLanguages.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CareerEvidence.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CareerProfileVersions.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CareerProfiles.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
deleted += await db.CvExtractionRuns.IgnoreQueryFilters().Where(item => item.OwnerUserId == owner).ExecuteDeleteAsync(cancellationToken);
@@ -1,6 +1,7 @@
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace JobTrackerApi.Services;
@@ -176,6 +177,26 @@ public sealed class ApplicationAssetsService : IApplicationAssetsService
match.RelevantProjects.Select(p => p.Subtitle is null ? p.Title : $"{p.Title} — {p.Subtitle}").ToList()));
}
var evidenceTerms = match.MatchedSkills.Concat(analysis.Keywords)
.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var evidence = await _db.CareerEvidence.AsNoTracking()
.Where(x => x.OwnerUserId == ownerUserId && x.IsVerified)
.OrderByDescending(x => x.UpdatedAtUtc)
.ToListAsync(ct);
var relevantEvidence = evidence.Where(item =>
{
var searchable = $"{item.Title} {item.Statement} {string.Join(' ', EvidenceTags(item.TagsJson))}";
return evidenceTerms.Any(term => searchable.Contains(term, StringComparison.OrdinalIgnoreCase));
}).Take(6).Select(item => $"{item.Title} — {item.Statement}").ToList();
if (relevantEvidence.Count > 0)
{
suggestions.Add(new TailoringSuggestionDto(
"career-evidence",
"Verified evidence to reuse",
"Facts you previously approved. Adapt the wording, but keep the claim accurate.",
relevantEvidence));
}
if (analysis.Keywords.Count > 0)
{
suggestions.Add(new TailoringSuggestionDto(
@@ -297,4 +318,10 @@ public sealed class ApplicationAssetsService : IApplicationAssetsService
private Task<JobApplication?> LoadJobAsync(string ownerUserId, int jobApplicationId, CancellationToken ct) =>
_db.JobApplications.AsNoTracking()
.FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
private static IReadOnlyList<string> EvidenceTags(string? json)
{
try { return JsonSerializer.Deserialize<List<string>>(json ?? "[]") ?? []; }
catch (JsonException) { return []; }
}
}
@@ -25,6 +25,7 @@ internal static class StartupSchemaOwnership
"Attachments",
"CareerCertifications",
"CareerEducations",
"CareerEvidence",
"CareerExperiences",
"CareerLanguages",
"CareerProfiles",