Skip to content

WW-98 / WING-103 · T1: SkillRecord + AcademicRecord contracts, EducationEntry.Id, TopSkills projection

Foundation ticket for the education/early-career epic. Work ONLY in this worktree (branch wt/WT-5988/WW-98, WorkWingman .NET src/ + Angular frontend/). Do NOT push — council-code-review gates before land.

Exact council-approved spec (Jira WING-103 description — authoritative, do not deviate)

  1. SkillRecord (replace the empty stub in src/WorkWingman.Core/Models/EducationStubs.cs) — three structural axes, NO aggregate score field anywhere — merge/collapse into a score must be impossible by construction (hard council ruling, non-negotiable):

    public class SkillRecord
    {
        public string Name { get; set; } = string.Empty;
        public SkillClaim? Claim { get; set; }
        public SkillProve Prove { get; set; } = new();
        public SkillAspire Aspire { get; set; } = new();
    }
    public class SkillClaim
    {
        public string? Level { get; set; }
        public ClaimSource Source { get; set; }   // Manual | StudyPlanCompletion
        public DateTimeOffset At { get; set; }
    }
    public class SkillProve
    {
        public List<ProveItem> Items { get; set; } = [];
    }
    public class ProveItem
    {
        public ProveKind Kind { get; set; }   // Course | Project | VerifiedWork | Cert | Endorsement
        public string RefId { get; set; } = string.Empty;
        public double Confidence { get; set; }
        public DateTimeOffset At { get; set; }
    }
    public class SkillAspire
    {
        public List<string> GapForRoles { get; set; } = [];
        public List<string> PlanItemIds { get; set; } = [];
    }
    public enum ClaimSource { Manual, StudyPlanCompletion }
    public enum ProveKind { Course, Project, VerifiedWork, Cert, Endorsement }
    
    Check HonestyGuard's stub methods (src/WorkWingman.Core/Services/HonestyGuard.cs) — FilterMaterialSkills needs to read .Prove.Items, confirm the shape lines up.

  2. AcademicRecord (replace the empty stub) — shared transcript/DARS schema:

    public class AcademicRecord
    {
        public string EducationEntryId { get; set; } = string.Empty;
        public AcademicSourceKind SourceKind { get; set; }   // Transcript | Dars | Manual
        public string Institution { get; set; } = string.Empty;
        public DateTimeOffset AsOfDate { get; set; }
        public string? RawTextRef { get; set; }
        public List<AcademicTerm> Terms { get; set; } = [];
        public List<AcademicCourse> Courses { get; set; } = [];
        public double? CumulativeGpa { get; set; }
        public int? CreditsEarned { get; set; }
        public int? CreditsInProgress { get; set; }
        public List<RequirementRemaining> RequirementsRemaining { get; set; } = [];
        public int? PriorCredits { get; set; }
        public int? ClepEligibleCount { get; set; }
    }
    public class AcademicCourse
    {
        public string Code { get; set; } = string.Empty;
        public string Title { get; set; } = string.Empty;
        public double? Credits { get; set; }
        public string? Grade { get; set; }
        public string Status { get; set; } = string.Empty;
        public string? TermId { get; set; }
    }
    public class RequirementRemaining
    {
        public string Category { get; set; } = string.Empty;
        public string Description { get; set; } = string.Empty;
        public double? CreditsNeeded { get; set; }
        public string Status { get; set; } = string.Empty;
        public List<string> CoursesSatisfying { get; set; } = [];   // structured, not free-text strings
    }
    public enum AcademicSourceKind { Transcript, Dars, Manual }
    
    Design AcademicTerm yourself (minimal — id/label/dates). T3/T4 (WW-100/101) own the parsing logic that populates this; you own the contract shape only. RequirementsRemaining structured (not strings) feeds the later Degree Advisor (WW-107/T10) — keep it queryable, not prose.

  3. EducationEntry.Id migration — EducationEntry (IntakeProfile.cs:99) currently has no Id. Add a stable Id, explicit migration for already-persisted profiles (existing entries without an Id get one assigned on load, don't crash old data). Check sibling entry types nearby for the id convention already used in this file.

  4. TopSkills stays a derived back-compat projection — NOT persisted. Read-time computation only.

  5. SkillTrack (replace empty stub):

    public class SkillTrack
    {
        public string TargetRole { get; set; } = string.Empty;
        public List<string> JobPostingIds { get; set; } = [];
        public List<string> RequiredSkills { get; set; } = [];
    }
    
    Ready/grow computed at read. "N skills away" = count of RequiredSkills with empty Prove, computed at read, never persisted.

  6. New LocalJsonStore collections: education, skills, skilltracks — NOT nested in the intake doc (council flagged: write amplification + the Windows UAE-not-IOException sharing-violation trap already hit once in this codebase — see existing LocalJsonStore usage for the correct collection-per-concern idiom).

Constraints

  • Do not implement HonestyGuard's real logic (that's WW-99/T2 — depends on YOUR contracts existing first). Just make sure the types you define match what HonestyGuard.cs's stub signatures already reference, so T2 isn't blocked by a shape mismatch.
  • Do not build any UI. Backend contracts + LocalJsonStore wiring only, plus a TS mirror in frontend/src/app/core/models.ts if the epic's frontend surfaces will eventually need these types (check whether WW-97's models.ts already stubbed placeholders — match the existing convention there).
  • EducationStudyPlan stub (4th empty class in EducationStubs.cs) is WW-98 scope too if you have capacity, but WW-106/T9 owns its real design (StudyPlan {DegreeCourse|Cert|Project|CLEP}) — a minimal placeholder shape here that doesn't block T9 is fine; don't over-design it.
  • No aggregate/composite skill scoring, anywhere. This was an explicit council kill of two seats' proposals — treat it as non-negotiable.

Gates (run yourself, report exit codes + counts)

  • dotnet build WorkWingman.slnx -c Debug
  • dotnet test tests/WorkWingman.Tests/WorkWingman.Tests.csproj --filter "FullyQualifiedName~SkillRecord|FullyQualifiedName~AcademicRecord|FullyQualifiedName~EducationEntry|FullyQualifiedName~TopSkills|FullyQualifiedName~LocalJsonStore"
  • If touching frontend: npx tsc -p tsconfig.json --noEmit

Done =

Clean build + green new tests + a WW-98-RESULT.md (files touched, final shape of each contract, migration approach for EducationEntry.Id, LocalJsonStore collection names). Commit locally on branch wt/WT-5988/WW-98. Do NOT push.