fix(career): preserve reviewed profile values
CI and Deploy / test (pull_request) Successful in 4m39s
CI and Deploy / deploy (pull_request) Has been skipped

Keep extraction heuristics out of manual save, version, and import paths so reviewed locations, URLs, dates, and languages round-trip unchanged.
This commit is contained in:
cesnimda
2026-08-15 16:56:31 +02:00
parent a6cffe0473
commit f0b9b222ff
15 changed files with 277 additions and 37 deletions
@@ -49,6 +49,158 @@ public static class StructuredCvProfileJson
return JsonSerializer.Serialize(Normalize(profile), SerializerOptions);
}
// Stored Career Profile values have already passed extraction review. Persistence therefore
// performs structural cleanup only: trim/dedupe empty values, but never reinterpret a user's
// website path, free-form date, location, role, institution or language. Extraction continues
// to use Normalize/Serialize above and keeps its stricter heuristics.
public static StructuredCvProfile DeserializePersisted(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return NormalizeForPersistence(new StructuredCvProfile());
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
var sections = JsonSerializer.Deserialize<List<StructuredCvSection>>(json, SerializerOptions) ?? new List<StructuredCvSection>();
return FromSections(sections);
}
if (doc.RootElement.ValueKind != JsonValueKind.Object) return NormalizeForPersistence(new StructuredCvProfile());
var profile = JsonSerializer.Deserialize<StructuredCvProfile>(json, SerializerOptions) ?? new StructuredCvProfile();
return NormalizeForPersistence(profile);
}
catch
{
return NormalizeForPersistence(new StructuredCvProfile());
}
}
public static string SerializePersisted(StructuredCvProfile? profile)
=> JsonSerializer.Serialize(NormalizeForPersistence(profile), SerializerOptions);
public static StructuredCvProfile NormalizeForPersistence(StructuredCvProfile? profile)
{
profile ??= new StructuredCvProfile();
profile.Version = string.IsNullOrWhiteSpace(profile.Version) ? "1" : profile.Version.Trim();
profile.Metadata ??= new StructuredCvMetadata();
profile.Metadata.Fields ??= new Dictionary<string, StructuredCvFieldMetadata>();
profile.Contact ??= new StructuredCvContact();
profile.Contact.FullName = TrimOrNull(profile.Contact.FullName);
profile.Contact.Headline = TrimOrNull(profile.Contact.Headline);
profile.Contact.Email = TrimOrNull(profile.Contact.Email);
profile.Contact.Phone = TrimOrNull(profile.Contact.Phone);
profile.Contact.Location = TrimOrNull(profile.Contact.Location);
profile.Contact.Website = TrimOrNull(profile.Contact.Website);
profile.Contact.LinkedIn = TrimOrNull(profile.Contact.LinkedIn);
profile.Summary = CleanList(profile.Summary);
profile.Jobs = (profile.Jobs ?? new List<StructuredCvJob>())
.Select(job =>
{
job ??= new StructuredCvJob();
job.Id = TrimOrNull(job.Id);
job.Title = TrimOrNull(job.Title);
job.Company = TrimOrNull(job.Company);
job.Location = TrimOrNull(job.Location);
job.Start = TrimOrNull(job.Start);
job.End = TrimOrNull(job.End);
job.StartDate = TrimOrNull(job.StartDate);
job.EndDate = TrimOrNull(job.EndDate);
job.Bullets = CleanList(job.Bullets);
job.Skills = CleanList(job.Skills);
return job;
})
.Where(job => job.Title is not null || job.Company is not null || job.Location is not null
|| job.Start is not null || job.End is not null || job.Bullets.Count > 0 || job.Skills.Count > 0)
.ToList();
profile.Education = (profile.Education ?? new List<StructuredCvEducation>())
.Select(education =>
{
education ??= new StructuredCvEducation();
education.Id = TrimOrNull(education.Id);
education.Qualification = TrimOrNull(education.Qualification);
education.QualificationLevel = TrimOrNull(education.QualificationLevel);
education.Institution = TrimOrNull(education.Institution);
education.Location = TrimOrNull(education.Location);
education.Start = TrimOrNull(education.Start);
education.End = TrimOrNull(education.End);
education.StartDate = TrimOrNull(education.StartDate);
education.EndDate = TrimOrNull(education.EndDate);
education.Details = CleanList(education.Details);
return education;
})
.Where(education => education.Qualification is not null || education.QualificationLevel is not null
|| education.Institution is not null || education.Location is not null || education.Start is not null
|| education.End is not null || education.Details.Count > 0)
.ToList();
profile.Certifications = (profile.Certifications ?? new List<StructuredCvCertification>())
.Select(certification =>
{
certification ??= new StructuredCvCertification();
certification.Id = TrimOrNull(certification.Id);
certification.Name = TrimOrNull(certification.Name);
certification.Issuer = TrimOrNull(certification.Issuer);
certification.Location = TrimOrNull(certification.Location);
certification.Date = TrimOrNull(certification.Date);
certification.DateNormalized = TrimOrNull(certification.DateNormalized);
certification.Details = CleanList(certification.Details);
return certification;
})
.Where(certification => certification.Name is not null || certification.Issuer is not null
|| certification.Location is not null || certification.Date is not null || certification.Details.Count > 0)
.ToList();
profile.Projects = (profile.Projects ?? new List<StructuredCvProject>())
.Select(project =>
{
project ??= new StructuredCvProject();
project.Id = TrimOrNull(project.Id);
project.Name = TrimOrNull(project.Name);
project.Role = TrimOrNull(project.Role);
project.Location = TrimOrNull(project.Location);
project.Start = TrimOrNull(project.Start);
project.End = TrimOrNull(project.End);
project.StartDate = TrimOrNull(project.StartDate);
project.EndDate = TrimOrNull(project.EndDate);
project.Bullets = CleanList(project.Bullets);
project.Skills = CleanList(project.Skills);
return project;
})
.Where(project => project.Name is not null || project.Role is not null || project.Location is not null
|| project.Start is not null || project.End is not null || project.Bullets.Count > 0 || project.Skills.Count > 0)
.ToList();
profile.Skills = CleanList(profile.Skills);
profile.Languages = (profile.Languages ?? new List<StructuredCvLanguage>())
.Select(language =>
{
language ??= new StructuredCvLanguage();
language.Name = TrimOrNull(language.Name);
language.Level = TrimOrNull(language.Level);
language.Notes = TrimOrNull(language.Notes);
return language;
})
.Where(language => language.Name is not null)
.ToList();
profile.Interests = CleanList(profile.Interests);
profile.Awards = CleanList(profile.Awards);
profile.Publications = CleanList(profile.Publications);
profile.Organisations = CleanList(profile.Organisations);
profile.References = CleanList(profile.References);
profile.OtherSections = (profile.OtherSections ?? new List<StructuredCvOtherSection>())
.Select(section => new StructuredCvOtherSection
{
Title = TrimOrNull(section?.Title),
Items = CleanList(section?.Items),
})
.Where(section => section.Title is not null || section.Items.Count > 0)
.ToList();
var normalizedSections = NormalizeSections(profile.Sections);
profile.Sections = normalizedSections.Count > 0 ? normalizedSections : BuildSections(profile);
return profile;
}
public static StructuredCvProfile Merge(StructuredCvProfile? preferred, StructuredCvProfile? fallback)
{
var primary = Normalize(preferred);