My learning path

The Learning app does not just list courses. It leads a signed-in learner into a complete learning path: a few questions about the project they want to build, then the courses, then a schedule. The route has eight steps, LearningSchedule.TotalSteps:

Steps Stage (LearningSchedule.Stage) Where it happens
1–6 Questions: the LearningRoadmap questions (project, outcome, experience, confidence, pace, language) the learner's plan, {viewer}/LearningRoadmap/Plan
7 PickCourses: the questions are done but the route holds no course the plan page lists every course on the instance with Add to my path
8 Schedule: courses are planned, and some lack a start or finish day each stop on the plan page carries its date inputs
✓ Complete: every planned course is dated or kept for later the front door names what is due next

NoPlan is the state before step 1.

The front door (Edu/Courses)

The hero card (MyLearning.Project) always shows one button that resumes exactly where the learner stands, the step ("Step 7 of 8") and the platform's progress control:

A saved legacy plan (from before the guided roadmap) just opens. The roadmap page itself offers to rebuild it.

Every course card carries the path action (MyLearning.PathActions):

The start page — a card only while it is timely

The signed-in start page (/, the person's own home) shows ONE learning card while it has something to say. The Learning app contributes it as data: Edu/HomeCards/LearningPath is a UiContribution with context: Home that embeds the LearningPathNudge area of Edu/Courses through the home's @@("area/HomeCards") region (a core addition: the Home contribution context). The card is the front door's hero, the same line, step and one resume button (MyLearning.Nudge), shown only when:

A concluded path, a saved legacy plan (its own page offers the rebuild) and an anonymous visitor see nothing. The start page motivates; the "Next: …" line stays on the Learning front door. A failed read keeps the card hidden rather than nudging off a guess.

The plan page

The roadmap page's questionnaire progress now counts against the whole route ("Step 2 of 8"). After the questions, a guide panel under the hero shows the step, the progress and the one line of what to do next (GuideStep / GuideLine). Two computed slots join the stops:

They are computed slots for the same reason the stops already were: each button or input addresses its own course key, which a bound row cannot carry.

Questions the courses declare

A course declares its own roadmap questions on its root, beside its modules: declared, not derived.

"learning": {
  "milestones": [ … ],
  "questions": [
    { "id": "data-source", "text": "Where does your data live today?", "kind": "choice",
      "topics": ["data-modeling"],
      "options": [
        { "value": "excel", "label": "In spreadsheets", "topics": ["data-import"] },
        { "value": "db",    "label": "In a database",   "topics": ["data-modeling"] } ] } ]
}

The assistant that compiles the path

Edu/Agent/LearningPathCompiler (a package agent with a distinct id, not part of the built-in roster) receives one message built by LearningPathCompiler.Prompt: the plan node, today as a calendar day, the base answers, each course question with its answer, the installed courses only (size, language, topics, modules m00, m01, …), and the courses already on the route, marking the ones the learner picked. It chooses and orders the courses, proposes start, finish and module days, and writes them to the plan's content.courses. It keeps every picked course and every note, and moves a dropped recommendation to previousPlan rather than deleting it. The thread runs in the learner's plan, with the plan as its context, and opens for them to follow. The rule-based route stays the default, so a path never depends on a model being configured.

The data — the learner's own typed node

Everything is stored on the learner's own plan node (Edu/LearningJourney, LearningJourneyContent). Nothing is copied into a layout-area replica:

Every date input is a DateTimeControl bound directly to the plan node at courses/{key}/targetStart, …/targetFinish and …/milestones/{m}/due, the same way the stop's notes already were. There is no save button and no /data copy.

🚨 Dates are calendar facts. A target is the day the learner picked. It is shown as that day (LearningSchedule.DateText), compared as a day, and never converted to a time zone; a test pins that 23:30 UTC on the 12th still reads "12 March". "Today" for the overdue check is UTC's date: a target is a day, and a boundary a few hours off for one viewer only changes whether that day already counts as overdue.

A course the learner adds is marked Picked, and a rebuilt recommendation keeps picked courses on the route; only recommended courses outside the new route move to an earlier plan. Completion for the next due item reads the plan's own courses as well as the catalog's, so a finished course the catalog no longer lists is never advertised as due.

Adding never duplicates (LearningJourney.AddFromCatalog). A course already on the route is left exactly as it is, and an entry kept from an earlier plan comes back with its own dates. Rebuilding the route after new answers keeps the learner's schedule: the course dates, and each module's date matched by its page wherever it now sits.

LearningCourse has value equality including the module map. A record compares an ImmutableDictionary by reference, which made two identical recommendations read as different.

Tests

Edu/LearningJourney → CourseQuestionsTests (the union, de-duplication and twins, answers feeding the route, the assistant's message, the panel); Edu/CourseCatalog → MyLearningTests.Nudge_OnlyWhileTimely (no course installed, anonymous, no app, each open stage, concluded, legacy, German).

Edu/LearningJourney → LearningScheduleTests (every route state, next due item, calendar dates, adding, rebuild, the guide, the add confirmation) and Edu/CourseCatalog → MyLearningTests.Hero_LeadsThroughEveryState (no plan, questions, no match, partial, complete, German) and Cards_OfferAddOrSchedule.