Feat/dynamic courses - #53
Merged
Merged
Conversation
…ure flag, fixture LLM Adds the data foundation for per-user runtime course generation: course schema nodes with prerequisites and criticality, learner profile, per-node state, learning events, renders, probes, attempts, feedback, term explanations, LLM usage log and audit log. DYNAMIC_COURSES_MODE gates every new path; with it off the v1 static pipeline behaves identically. FixtureLLMService allows the whole pipeline to be tested without API keys.
The LLM emits OpenUI Lang; the parser runs server-side in Python and produces a validated UISpec IR. The browser never receives generated markup. Ten frozen components. The 12-component / 5-root-child ceiling is a contract rule, not styling: working memory handles 4-7 elements. QuizItem carries no correct answer — that lives in answer_key and is never serialized to the client.
The AI proposes only the index — competency nodes, prerequisites, criticality — and the creator validates it before any content exists. The gate is blocking and rejects before creating a job, so a course whose schema is not validated cannot generate content. Covered by test_schema_gate.py.
Five questions, one per screen, ~90 seconds, skippable. Each question changes something real in generation: role and sector enter the prompt, experience level inverts the scaffolding, the preset drives presentation. Deliberately does NOT ask for neurodivergence diagnoses — that is health data under GDPR art. 9 and unnecessary: question 5 asks about reading needs, which produces the same functional result without the legal exposure. DELETE /users/me/learner-profile erases all seven personal tables (art. 17).
Two items before a node opens (three if critical). If the learner already knows it, the node is skipped — an eight-year veteran no longer sits through an introduction to their own job. The mastery rule is computable and every threshold is reachable after the renormalised tiebreak. All eight state transitions are tested, plus cold start with no profile data and the two-correct-by-chance false positive.
Click any word in course content and get it explained in its surrounding context. Explanations are cached per term and context. User-supplied term and context are fenced with a per-request nonce rather than quotes, and length-capped, so a closing quote cannot hijack the instruction.
Renders the validated IR with the repo's own components — no new npm dependency, no charting library (inline SVG), and nothing generated reaches the DOM as markup. ClickableSurface hit-tests with closest() as the first line of the handler, so a click on a button, a quiz option or a link does not fire an explanation. Only prose does.
v2-dynamic-courses.md is the implementation spec: architecture, data model, pipeline, render contract, onboarding, mastery rule, latency strategy, feature flag, API, test strategy and batch plan. openui-adoption.md records what was measured about OpenUI Lang: the grammar we emit is the standard's, but @openuidev's parser accepts three of our six malformed fixtures silently and checks no types or enums, so the Python parser stays. Also records that the standard does support reactive state — the earlier note claiming it is one-shot is obsolete.
- GDPR art. 17 erasure was deleting five of seven personal tables and still returning 204: node_attempts and node_probes, the two holding the learner's own submitted answers, were left behind - POST /schema/propose had no in-flight guard, so every call spawned another full schema-designer job on the same course and two could race writing the same nodes - ProbeSession carried the answer_key row out to its caller; services now return an explicit schema without it, with a serialization test asserting the key never appears - the in-process explain rate-limiter map grew one entry per user and was never pruned - QuizItemBlock's hints_used is documented as informational: a client-controlled value must not govern revealing the correct answer
@openuidev/lang-core and @openuidev/react-lang (pinned exactly, no caret — the
API is 0.2.x and will break) plus zod. The ten kit blocks are now registered via
defineComponent with zod schemas and rendered by their <Renderer> with streaming,
so the design system is preserved and we stop maintaining a bespoke renderer.
@modelcontextprotocol/sdk is deliberately not installed: no bundle imports it.
Reactivity stays OFF, measured reasons in docs/design/openui-adoption.md:
teaching the model tools enables the entire expression syntax at once (bindings:
false still teaches $var), queries auto-fire on a useEffect with no click and
refreshInterval has no cap, and tool names can be computed expressions so any
text-based allowlist is incomplete.
The Python parser stays as the gate before persisting, in a reduced role. Not
out of not-invented-here: the frozen grammar has no {, @, $ or ?:, so reactive
constructs are structurally inexpressible rather than blocklisted. The browser
receives text re-serialized from a validated UISpec, never the model's raw
output.
The runtime graph decides the format, generates the UI, validates it and repairs it on failure, falling back to the seed when repair also fails. Renders are cached by profile bucket, not by user, so the second learner in the same bucket costs no LLM call. Calibration is a hard rule: under three completed nodes the format vector does not enter the cache key and decide_formato is not called at all — the learner must build a mental map before the interface starts moving. Closes the seams B3 and B4 left without a caller: profile signals, event recording and the unmastered-prerequisite join are now wired to the node routes.
The node view is where dynamic generation becomes visible: the learner answers the pre-assessment while the content generates behind it, so the wait is work rather than a spinner. The admin schema screen is the creator's gate — validation stays disabled until every node is reviewed, and detected cycles are surfaced instead of swallowed. ProtectedRoute treats a 404 from the profile endpoint as 'do not redirect': if it meant 'not onboarded', turning the flag off mid-session would trap the user in a redirect loop to a route that no longer exists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.