feat: add career profile foundation with versioned history

Introduces the Career Workspace's bounded data foundation, additive
and backwards-compatible: ApplicationUser.ProfileCvStructureJson
stays the authoritative column every existing read path uses; the new
CareerProfiles/CareerProfileVersions tables mirror it via
ICareerProfileService so future Career Workspace features (variants,
history UI) have real tables to build on rather than starting a
second migration later.

- CareerProfile: one snapshot row per user (Version, ProfileJson).
- CareerProfileVersion: append-only history, one row per save
  (upload/rebuild/improve/reprocess/parse), so a profile edit is never
  silently lost the way ProfileCvStructureJson overwrites are today.
- Stable item IDs assigned to jobs/education/certifications/projects
  on first save and preserved across later saves -- the prerequisite
  for CV variants to reference "this job" by identity instead of
  array position.
- CvDateNormalizer: best-effort free-string -> "YYYY-MM" parsing for
  job/education/certification/project date ranges, kept alongside
  (never replacing) the original free-string fields.
- Both SQLite (dev) and MySQL/MariaDB (prod) reconciler dialects,
  matching this repo's schema-via-raw-SQL-reconciler convention
  rather than EF migrations.

Job tracking is untouched -- this is entirely within the profile/CV
domain per the Career Workspace product boundary.
This commit is contained in:
cesnimda
2026-07-12 15:18:05 +02:00
parent a4c8e4ac5d
commit 235e291d8f
8 changed files with 444 additions and 8 deletions
+16
View File
@@ -49,11 +49,19 @@ public sealed class StructuredCvContact
public sealed class StructuredCvJob
{
// Stable item ID (assigned by CareerProfileService on first save). Required for CV variants
// to reference "this job" across profile edits, instead of by array position. Nullable/empty
// on freshly-parsed or legacy data until the first save assigns it.
public string? Id { get; set; }
public string? Title { get; set; }
public string? Company { get; set; }
public string? Location { get; set; }
public string? Start { get; set; }
public string? End { get; set; }
// Best-effort "YYYY-MM" normalization of Start/End, computed alongside Id assignment.
// Null when Start/End can't be parsed; the free-string fields above remain the display source.
public string? StartDate { get; set; }
public string? EndDate { get; set; }
public bool IsCurrent { get; set; }
public List<string> Bullets { get; set; } = new();
public List<string> Skills { get; set; } = new();
@@ -61,31 +69,39 @@ public sealed class StructuredCvJob
public sealed class StructuredCvEducation
{
public string? Id { get; set; }
public string? Qualification { get; set; }
public string? QualificationLevel { get; set; }
public string? Institution { get; set; }
public string? Location { get; set; }
public string? Start { get; set; }
public string? End { get; set; }
public string? StartDate { get; set; }
public string? EndDate { get; set; }
public List<string> Details { get; set; } = new();
}
public sealed class StructuredCvCertification
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Issuer { get; set; }
public string? Location { get; set; }
public string? Date { get; set; }
public string? DateNormalized { get; set; }
public List<string> Details { get; set; } = new();
}
public sealed class StructuredCvProject
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Role { get; set; }
public string? Location { get; set; }
public string? Start { get; set; }
public string? End { get; set; }
public string? StartDate { get; set; }
public string? EndDate { get; set; }
public List<string> Bullets { get; set; } = new();
public List<string> Skills { get; set; } = new();
}