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)¶
-
SkillRecord(replace the empty stub insrc/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):Checkpublic 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 }HonestyGuard's stub methods (src/WorkWingman.Core/Services/HonestyGuard.cs) —FilterMaterialSkillsneeds to read.Prove.Items, confirm the shape lines up. -
AcademicRecord(replace the empty stub) — shared transcript/DARS schema:Designpublic 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 }AcademicTermyourself (minimal — id/label/dates). T3/T4 (WW-100/101) own the parsing logic that populates this; you own the contract shape only.RequirementsRemainingstructured (not strings) feeds the later Degree Advisor (WW-107/T10) — keep it queryable, not prose. -
EducationEntry.Idmigration —EducationEntry(IntakeProfile.cs:99) currently has noId. Add a stableId, 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. -
TopSkillsstays a derived back-compat projection — NOT persisted. Read-time computation only. -
SkillTrack(replace empty stub):Ready/grow computed at read. "N skills away" = count ofpublic class SkillTrack { public string TargetRole { get; set; } = string.Empty; public List<string> JobPostingIds { get; set; } = []; public List<string> RequiredSkills { get; set; } = []; }RequiredSkillswith empty Prove, computed at read, never persisted. -
New
LocalJsonStorecollections: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 existingLocalJsonStoreusage 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.tsif the epic's frontend surfaces will eventually need these types (check whether WW-97'smodels.tsalready stubbed placeholders — match the existing convention there). EducationStudyPlanstub (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 Debugdotnet 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.