Neutral architecture of learning experiences
Date: 2026-08-14
Status: vertical slice implemented behind rollout
Applies to: dynamic v2 courses, experience selection and future providers
Authority: this document defines the boundary between persisted truth, episodic direction,
capability selection and render. v2-dynamic-courses.md keeps the
specification of the v2 path and the historical fallback; didact-integration.md
describes Didact’s executable inventory.
The relationship between modality and internal structure is expanded in
delivery-modalities.md. Audio and video are representations that can
live inside an experience; they are not navigation destinations or tabs the person has to choose.
1. Decision
The published course keeps its constitution: what competency matters, what source truth backs it, what errors are critical and what evidence lets us claim mastery. It does not keep a sequence of screens, a modality, a visual variant or a component as pedagogical truth.
The presentation is decided when the person opens the node. The server builds a grounded
EpisodeBrief adapted to their state; it then filters the catalog by capabilities and pins a
versioned implementation. The resulting output is anchored so a refresh does not change the
activity mid-attempt.
published course
CompetencyContract + SourceAffordanceMap + EvidenceGate
│
person's current state
│
▼
EpisodeBrief on-the-fly
(mission, action, evidence, limits, continuation)
│
▼
CapabilityBroker → honest shortlist of 1–3
│
▼
ExperienceResolver → binding/definition pinned
│
▼
LearningExperience + shell_mode
│
▼
server-owned evidence → mastery → continuation
Didact is today’s main educational provider, not SkillNet’s ontology. skillnet.text-content
and media.checkpoint-video cross the same boundary. Adding a simulation or a code lab must not
modify the course contract, the episodic director or LearningExperience.
2. What is fixed and what is generated
| Fixed when the course is validated/published | Decided when the node is opened |
|---|---|
| Outcome, criticality and prerequisites | Concrete mission for this attempt |
CompetencyContract and its version |
EpisodeBrief and budget |
| Required facts and provenance | Dominant action and amount of support |
SourceAffordanceMap, revisions and digests |
Representation suited to the task and context |
EvidenceGate, private oracles and critical errors |
Shortlist of available capabilities |
| Mastery policy | Binding and definition pinned for the render |
| Versioned catalog and security policies | shell_mode and continuation conditions |
Videos, games, alternative sequences, pedagogical baselines or speculative presentation artifacts are not pre-created for each course. Knowledge packs, affordance maps, indexes and oracles are not “course content already assembled”: they are the reproducible truth and the limits from which the runtime can generate without inventing.
node_renders persists the validated result that was actually served, for cost, auditing and
reproducibility purposes. That cache is the consequence of a runtime decision, not a
presentation plan prepared during publication.
2.1 Anticipatory generation during the session
“On-the-fly” describes when the experience is decided, it does not force waiting with an empty screen. The decision still uses the current constitution, the learner’s state and the runtime policy, but it can run a few steps before the person opens the lesson:
- when the course map is opened, the client requests the first two available lessons in the background;
- once a lesson already has a served render, the client keeps a moving window with the next three lessons requested;
- as the learner advances, the window shifts and prepares the new lesson coming up ahead;
- repeated requests are idempotent: a render that is ready or in progress does not start another generation;
- opening a lesson fixes its render, so refreshing, answering or going back does not silently change what the person was looking at.
This does not make the course static nor mix presentation artifacts with its definition.
These are anticipated, cached runtime renders. The full course is not generated, all branches
are not chosen ahead of time, and invalidation still depends on versions, digests and
generation_policy_key. The window today uses lessons from the published path; once a
competency admits several internal episodes or probabilistic branches, the same rule must apply
to eligible continuations, not to fabricating every alternative.
3. Stable contracts
3.1 CompetencyContract
Defines the job outcome, mandatory facts, criticality, prerequisites, evidence gates, critical
errors and mastery_policy_ref. It does not admit provider, component, slot or layout. A change
from Timeline to video cannot change a safe procedure or lower the required evidence.
3.2 SourceAffordanceMap
Fixes the exact available sources and revisions, their digests and which actions they honestly support: inspecting a procedure, recognizing a state, ordering steps or executing in a sandbox, for example. An affordance can be exact, derived or synthetic, but it always references the source that backs it. If the source does not allow an action, the generator must not simulate that it does.
3.3 EpisodeBrief
It is a learning beat created on-the-fly and free of provider names. It contains:
- an exact reference to competency, grounding and the person’s state;
- a mission with a single dominant action;
- authorized source and affordances;
- assessment mode and gates, if demonstrable;
- critical errors and recovery;
- content, interaction, media and latency budget;
- continuation conditions, including a default exit.
It does not contain ScreenScheme, lead, concept, practice, component_id, provider nor
layout instructions. Explanation, practice and transfer remain possible resources, not a recipe
that must always appear or appear in that order. The coherent unit is the complete mission: it
can span a vertical scroll and contain several pieces if all of them serve the same dominant
action.
4. Tickets and SQL are not the same experience
The abstraction exists precisely to allow radical differences without creating two platforms.
| Retrieve tickets | SQL | |
|---|---|---|
| Job outcome | Attend to a customer and retrieve the correct ticket without exposing or confusing data | Build or fix a query that produces the requested result |
| Source truth | Current provider manual, visible fields, exceptions and process limits | Schema, test data, dialect and execution constraints |
| Plausible dominant action | Diagnose the case and choose/execute the next operational step | Write, run and debug a query |
| Required affordance | Faithful operational case, search, states and process decisions | Executable editor and sandbox with resettable state |
| Valid evidence | Correct decision and sequence, including handling of critical errors | Executed result, tests and explanation of the failure |
| Resulting experience | Guided case or attention simulation with pinpoint reference to the manual | Code lab with engine feedback and test cases |
A quiz can serve as a limited check in both domains, but it does not replace operational transfer or execution. The broker must decline rather than present a low-fidelity imitation as evidence of mastery.
5. Broad catalog, small context
Each implementation publishes a versioned CapabilityDescriptor, framework-neutral: learner
actions, producible evidence, affordances, accessibility, security, ports, latency and quality.
The full catalog can grow without entering entirely into the prompt or the browser’s initial
bundle.
At runtime, CapabilityBroker applies hard gates in this conceptual order:
- learner action;
- required evidence;
- real affordances of the source;
- accessibility;
- security;
- available ports;
- latency budget.
Only afterward does it rank the valid candidates deterministically. It returns an honest
shortlist of one to three; it does not pad the minimum with worse options. If none survives, it
can widen the catalog once, applying exactly the same gates. If there is still no valid option,
it returns Declined(reason).
The broker filters capabilities. ExperienceResolver then takes that list and pins a
published binding (experience_id, implementation_ref, definition_ref). This separation
prevents the pedagogical policy from knowing provider details. On the frontend,
ExperienceAdapterRegistry lazily loads the adapter by implementation_ref; LearningExperience
does not contain a central switch nor static imports of the whole catalog.
6. Evidence, support and mastery
The evaluation authority is the server. Answer keys, oracles and rubrics remain in the private
definition. An evaluable implementation sends a stable attempt tied to experience_id; the
server validates the binding and translates the result into normalized evidence before applying
the mastery policy.
Writing an attempt, evidence and mastery must be idempotent and transactional. The client never writes score, outcome or mastery. Playback, click, dwell time and media completion are usage signals, not learning evidence.
support_only is an honest output when there is grounded material to help but no certified
capability to produce the required evidence. In that mode:
- the
EpisodeBriefusesassessment_mode: noneand does not referenceEvidenceGate; - the person may receive an explanation, example or operational reference;
- no event updates mastery or satisfies a gate;
- the continuation cannot declare the competency mastered.
Turning that support into evidence requires another evaluable experience, with a port, binding
and server-owned oracle. Adding a “completed” button or a playback checkpoint is not enough. A
competency without a wired oracle is never synthesized: it does not grant mastery and does not
turn into an invented quiz; it declines toward support_only (with grounded material) or toward
an explicit decline.
6.0 Grounding requirement: the knowledge pack
The grounded episodic shell requires a node_knowledge_packs in READY state for the node,
with the current schema_version and generator_version. Without it, direct_episode declines
with missing_knowledge_pack and the node falls back to legacy_stepper: this is the underlying
reason why a freshly seeded or merely validated course never reaches the episode.
Packs are produced only by the LLM-backed runner (run_packs_for_schema), and it is only
triggered from the schema generation agent and from PUT /{course_id}/schema. Neither seeding
nor POST /schema/validate create packs. That is why the public seed (seed_learning_demo,
via the create_course_end_to_end orchestrator) runs that same runner after validating each
course when a real generation LLM is configured: this way a seeded course is capable of
end-to-end episode delivery. With the fixture/local model there is nothing to ground, so the
step is skipped —the runtime stays on legacy_stepper, without breaking—. The runner is
fail-open: a missing pack degrades to legacy, it never brings down generation or seeding.
Operational consequence for the demo: a course must go through generation/PUT /schema (or
through LLM-backed seeding) to have READY packs; turning on ADAPTIVE_EPISODES alone is not
enough —it only enables the branch that later declines if there is no pack—.
6.1 Decline reason, preserved and server-only
When the episode declines or degrades, the exact policy code is frozen in the render’s
server-only provenance (GenerationProvenance.episode_decline_reason, alongside shell_mode and
episode_status). This way a declined + legacy_stepper render stops being an opaque symptom:
the reason —evidence_policy:pack_not_ready, critical_oracle_unavailable,
missing_knowledge_pack…— stays inspectable in traces and tests. It is a policy identifier, not
learner data, and like all of UISpec.generation it is excluded from dumps toward the client. A
decline is a safe fallback for a real failure, not an excuse to reduce the formula to
lead/concept/practice: the “oracle unavailable / unsupported” reasons keep the episodic shell as
support_only; only a real generation failure falls to legacy_stepper.
6.2 The job profile only enters if the source backs it
The learner’s job and sector do not enter the generative prompt unless the source itself
backs them. The check is structural (_profile_is_grounded in llm/prompts/runtime.py): it
tokenizes the job/sector and only injects them if either appears in the source text. When it
does not —a retail profile over a boxing source— the job is omitted entirely, not marked
“undeclared”: without a role to hang the examples on, the model cannot invent “a customer walks
up…” in a course that is not about customer service. There is no domain list; the profile only
adjusts difficulty, support and examples that are compatible, and the source and node always
prevail. The role_bucket keeps partitioning the cache even when the role does not enter the
text (bounded waste, never a wrong cross-match).
7. Episodic shell and modalities
shell_mode is a server-owned decision persisted with the render:
episode: shows a coherent mission in a single vertical flow. It does not group by “resolvable” components, it does not create slides and it does not introduce a second scroll inside the content.legacy_stepper: keeps exactly the historical navigation for older renders and for the rollout fallback.
The browser does not infer the shell from component names, provider, block order or the
presence of a quiz. A refresh returns the same render and the same fixed shell_mode.
Audio and video are embedded wherever the mission needs them. There are no Web/Audio/Video tabs nor three parallel versions of the same screen. Declared preferences, bandwidth, accessibility and observed effectiveness are selection signals, never a rigid taxonomy of “learning styles”. Video does not autoplay and requires subtitles or a transcript; audio needs controls, a transcript and an accessible alternative.
Continuation belongs to the episode and its evidence, not to the scroll position. A mission can continue to additional practice, recovery, the next state or human review depending on its conditions; the shell only presents that decision.
7.1 Multi-screen episode, content vs. evaluation, critic and pre-warm
Multi-screen. An episode is no longer ONE screen. Each direct child of the root Stack is
a SCREEN the learner steps through one by one (frontend pagination, no scroll)
(llm/prompts/runtime.py, version note episode/3, and the _EPISODE_MULTISCREEN block). The
natural ceiling is the validator’s cap: MAX_ROOT_CHILDREN = 5 (render/spec.py). The first
child is always a TextContent variant="lead" (rule 7 of spec.py). Focus rule: A SINGLE
FOCUS PER SCREEN —one idea, OR a set of definitions, OR an interaction, never all three. A
simple node is one screen; a rich one is several. The deterministic schema planner
(agents/runtime/screen_scheme.py) distributes three lead/concept/practice slots, and the
concept block takes shape from the material (Table, Chart, StepSequence, BeforeAfter),
never prose. Catalog-agnostic post-hoc evaluation lives in agents/runtime/screen_eval.py.
Content vs. evaluation. The most important distinction in the prompt
(_EPISODE_QUALITY_RULES, runtime.py): CONTENT/RESOURCE teaches or helps study (TextContent,
Table, BeforeAfter, DidactTimeline, and also Flashcard) and never certifies;
EVALUATION/TEST is a real check (QuizItem, DragOrder or a server-side experience). A content
block used as a test is an error even if the schema validates. The agents/runtime/assessment.py
planner picks the closing test with a deterministic rotation (DIDACT_CLOSER_ROTATION);
Flashcard was retired as a closer on 2026-08-17 because it turned every “evaluation” into a
simple reveal.
Certification vs. support_only. The policy lives in
src/services/evidence_contract_policy.py. It is only certified (accept,
evidence_type="grounded_fact_recognition" with oracle_ref) for a RECOGNIZE mission whose
atoms are all in {FACT, PROCEDURE_STEP, CRITERION}, backed by a real deterministic scorer
(_UNSCORABLE guard). Otherwise the node declines to support_only with a typed reason
(CRITICAL_ORACLE_UNAVAILABLE, EXECUTION_ORACLE_UNAVAILABLE, RUBRIC_ORACLE_UNAVAILABLE,
REQUIRED_EVIDENCE_UNSUPPORTED). The projection guard in src/services/episode_inputs.py is
fail-closed: a critical node without safety atoms is downgraded to recommended instead of
dying, and evidence gates require a server-owned oracle_ref + evidence_type or they decline
(MISSING_REQUIRED_EVIDENCE). The provenance state is
episode_status ∈ {ready, support_only, declined, not_requested} (render/spec.py).
Lean generator + critic. With MULTI_AGENT_RENDER, the episode is composed by specialized
agents (agents/runtime/agents/): blueprint (decides the screen structure without writing
content; “the CONCEPT block is always interactive or structured, NEVER prose”), content_writer,
interaction_designer and assembler (plain Python, no LLM; forces the server-owned
LearningExperience into the root’s last child). The critic
(agents/runtime/agents/episode_critic.py) does one review of an episode that is already
valid (pedagogy, not syntax): a single focus per screen, content vs. evaluation kept separate
(flags a flashcard/reveal used as a test and any DidactGlossary), fit between component and
material, evaluation variety and a sensible number of screens. It is fail-open (_MAX_NOTES = 4,
any error → no review).
Pre-warm on validate. A validated course is servable, but its renders do not exist yet.
After validate (routes/course_schema.py), spawn_prewarm_first_nodes
(services/node_render_service.py) is launched, which warms the shared renders of the first
nodes in the background (with a synthetic default-bucket learner that never pins) so that the
first opening is an instant cache hit. It is superseded per course
(cancel_by_prefix(f"prewarm:{course_id}:")) and waits for the packs to be ready
(_PREWARM_PACK_WAIT_SECONDS = 300.0).
8. Compatibility and rollout
ScreenScheme and the historical screen formula remain solely as a rollout fallback. They are
used when ADAPTIVE_EPISODES is disabled, when the episodic director declines, or when serving
an already-pinned legacy render. They do not constrain the episodic prompt, they are not
projected inside EpisodeBrief, and they are not the product model for new courses.
The safe chain is:
- grounded episode with certified capability;
support_onlyepisode when it can help without asserting evidence;- explicit decline toward the
legacy_stepperpath during rollout; - visible and traceable error if no safe fallback exists either.
DidactActivity is kept as a read alias for historical publications. New renders cross
LearningExperience; adapters or legacy data are not removed while active references exist.
Incompatible versions and digests invalidate the cache and require explicit regeneration or
migration.
9. Simulations and level-3 GenUI
The registry already admits future providers, but registering a name does not create a real capability. A level-3 simulation or experience is only enabled when it provides:
- deterministic state model, transitions and invariants;
- explicit simulation or execution port;
- reproducible reset and isolation between attempts;
- server-owned oracle and translation to evidence;
- declared fidelity relative to the source and the work environment;
- accessible alternative and safe behavior on failure;
- met latency budget and honest fallback.
The generator selects recipes and typed definitions; it does not generate free executable code inside the OpenUI program. The first pilot must demonstrate at least one operational case such as an incident-management tool and another executable one such as SQL. If both require editing the core contract, the abstraction is not yet sufficient.
10. Rollout validation checklist
Docker and backend
- Rebuild the stack with
docker compose up -d --buildand confirm that API, web and database are healthy. - Enable
ADAPTIVE_EPISODESonly in the intended test environment and verify that the policy version separates legacy and episodic cache keys. - Use a validated dynamic course with a
readyknowledge pack, current sources and a node that does not yet have a pinned render. - Confirm in the
GET /nodes/{id}/renderresponse thatshell_modeisepisodeand that the render gets anchored; a second GET must return the same identity. - Test
ready,support_onlyand decline to legacy separately. Insupport_only, check that no evidence is created and mastery does not change.
Fresh node
- Validate with a new or explicitly regenerated node. A node with a previous
active_render_idmust keep serving its pinned version and does not demonstrate the new rollout. - Cover at least one operational-manual case and one SQL/code case; their actions, affordances and chosen components must be materially different.
- Confirm that facts, source revision, gates, critical errors and binding appear in the server-side trace without exposing private oracles to the client.
- Force a missing port or missing evidence and check
support_only/Declined, never a simulation or a faked evaluation.
Browser
- Open the episodic node on desktop and mobile viewport: one mission, one vertical page scroll, no Web/Audio/Video tabs, no slides and no nested content scroll.
- Check keyboard navigation, visible focus, screen reader, contrast and reduced-motion preference.
- For video/audio, verify absence of autoplay, controls, subtitles or transcript and an accessible alternative.
- Reload and go back/forward: identity, content and
shell_modemust remain stable. - Open a
legacy_stepperrender and confirm its historical navigation has not changed. - Run an evaluable evidence submission twice with the same
attempt_id: it must be idempotent. Repeat with asupport_onlyexperience: mastery must stay the same.
11. Success criteria
The abstraction is sufficient when:
- the course fixes truth and evidence, not a pre-created presentation;
- Tickets and SQL produce radically different episodes from the same runtime contract;
- the catalog can grow without enlarging the prompt or modifying the core;
- a shortlist can contain a single valid candidate or decline with an observable reason;
- every mastery update comes from server-owned evidence;
support_onlyhelps without certifying;shell_modedoes not depend on browser heuristics;- audio and video are part of the mission, not of a navigation across modalities;
ScreenSchemecan be retired once rollout ends without changingEpisodeBriefor the bindings;- a future simulation enters via descriptor, ports, definition, adapter and tests, not via a special branch in the planner.
12. Relationship to other documents
v2-dynamic-courses.mddefines v2 delivery, persistence, cache and mastery transition. Its screen recipes describe thelegacy_stepperfallback.node-knowledge-packs.mddefines the preparation of source truth that feedsSourceAffordanceMap.delivery-modalities.mddefines modality as an affordance and selection signal, not as a tab.didact-integration.mdlists the Didact provider’s real components, ports and blockers.didact-integration-strategy.mddevelops level-3 recipes and GenUI.openui-adoption.mdkeeps the dialect, security and renderer constraints.