Skip to content

Feat/dynamic courses - #53

Merged
JoseEstevez520 merged 12 commits into
mainfrom
feat/dynamic-courses
Jul 27, 2026
Merged

Feat/dynamic courses#53
JoseEstevez520 merged 12 commits into
mainfrom
feat/dynamic-courses

Conversation

@JoseEstevez520

Copy link
Copy Markdown
Collaborator

No description provided.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant