Migrating existing learner answers
The words learners have already written are the one thing here that cannot be regenerated. Every authored word is in git and a GitSync restores it; a learner's answer exists in exactly one place. So the migration onto data-bound exercises is staged: this page is the analysis and the plan. Nothing described here writes or deletes anything yet.
What is actually there
Before the authored/learner split, a writable exercise was one node in a per-learner copy of the course, holding both halves at once:
// {viewer}/AgenticPrimer/01-TheMagicWish/Exercise/ThreeWishesBook
{ "$type": "WishBookContent",
"title": "Your first wish", // authored
"intro": "Write your answers here.", // authored
"prompts": ["…", "…", "…"], // authored
"answer1": "a robot that tidies my room", // the learner's
"answer2": "…", "answer3": "…" // …up to answer6
}
WishBook is a course-local NodeType (AgenticPrimer/WishBook, AgenticPrimerDe/WishBook), not
an Edu/ one — so the shape lives in the course repos, and only the central definitions are visible
from outside a learner's space. Measured on memex.meshweaver.cloud, 2026-09-02: six WishBook
nodes, three in AgenticPrimer and three in AgenticPrimerDe. An install of a course copies its
whole subtree (AdvancedBusinessRules: 126 nodes), so every learner of those courses holds their own
copy of each of those six.
🚨 The finding that decides the design: nobody can survey this from outside
A legacy copy lives in the learner's own partition. A global admin holds no data access
anywhere on any mesh — Admin/_Access gates platform actions and grants zero read on any partition's
content — so there is no vantage point from which an operator can count the copies, read the answers,
verify a harvest, or check afterwards that nothing was lost. Attempting the survey as system would
mean reading every learner's private work to plan a migration, which is not a thing to do casually
and is not necessary.
So the migration is not a sweep. It is the learner's own act, on their own render turn. When a learner opens an exercise that has moved to the new shape, the page harvests their copy into their sheet, in their own circuit, under their own identity. No cross-partition read, no admin pass, no list of learners to work through, and no window in which somebody else's work is in flight.
That also means the honest answer to "how many learners are affected?" is: not knowable from outside, by design. What is knowable is which courses carry the legacy shape (the six nodes above), and that is the unit the rollout is planned in anyway.
The plan, as code
AnswerHarvest.Plan(viewerHome, copyPath, copyContent, existingSheet) returns a HarvestPlan — what
would move, what would not, and why. It is pure: content in, plan out, no hub and no clock, so
every rule below is a unit test rather than something a reviewer has to take on trust. Holding a
plan has no effect on anything.
Three properties make it safe to run, and to run again:
- Additive — nothing is deleted. The plan contains writes and nothing else; the legacy copy is never touched. The inverse of the whole migration is therefore "delete the sheet", with the original still sitting where it always was. Retiring the old copies is a separate decision, taken once somebody has confirmed the new side reads correctly — never a step of the harvest.
- The sheet always wins. An answer the sheet already holds is left alone. The sheet is the NEW
store, so what is on it is at least as recent as the copy; overwriting would destroy an answer the
learner wrote after their course moved.
HarvestPlan.MergeIntorestates the same rule where the write is shaped, so a caller cannot turn a plan into an overwrite by accident. - Authored text is never carried.
title,introandpromptsare the author's words. Copying them into the learner's partition would recreate exactly the mixture the new model exists to end, and leave a second copy of the prompts to go stale at the next authoring change.
Two smaller rules follow the same instinct: a blank answer is not work (carrying it would make an untouched question look answered), and a member the legacy shape does not know is reported rather than dropped — a course whose books differ must show up as a line to read, not as a learner who apparently never wrote anything.
The key convention — no mapping table
A legacy answer is stored under answerN; a workbook field's key is the pointer answers/{key} on
the sheet. So a WishBook converted to an Edu/Workbook keeps answer1…answerN as its field
keys, and the harvest is a rename-free copy. There is no per-exercise mapping to author, to get
wrong, or to keep in step with the questions.
A course that would rather rename its keys may — but its learners' answers then land under the old
ones, and it must move them itself. Describe(plan) names every key it carries, so that is visible
rather than assumed.
What execution will look like
Its own change, after a course actually adopts the new shape:
- On the workbook page, for a signed-in learner whose legacy copy exists, read the copy (their own
node, their own identity), build the plan, and apply it to the sheet — once, and only ever
additively.
Planis idempotent, so a second render carries nothing. - The
Ensure-then-write ordering is the existing one: the sheet is created empty on the render turn (create-only, never an upsert) before anything is merged onto it. - The page says what happened, in the learner's own words: "your earlier answers were brought across." A migration a learner cannot see is one they cannot check.
What is deliberately NOT planned here
- Deleting or freezing the old copies. Not part of the migration, at any stage. A course stops copying an exercise when it adopts the workbook; the copies already made stay until somebody decides otherwise, with their contents intact.
- Quiz responses. There are none to migrate — until the quiz moved onto the sheet its answers lived in a browser variable and were never persisted anywhere.
- Code cells. The copy-on-write model for them is still to be designed; a cell classified
ModifiedbyContentFingerprintis learner work, but it is a node, not a field, and it does not belong on an answer sheet.